psyche-ai 10.0.4 → 10.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Psyche — 面向智能体的 AI-first 主观性内核
2
2
 
3
3
  [![npm](https://img.shields.io/npm/v/psyche-ai)](https://www.npmjs.com/package/psyche-ai)
4
- [![tests](https://img.shields.io/badge/tests-1316%20passing-brightgreen)]()
4
+ [![tests](https://img.shields.io/badge/tests-1405%20passing-brightgreen)]()
5
5
  [![deps](https://img.shields.io/badge/dependencies-0-blue)]()
6
6
  [![license](https://img.shields.io/badge/license-MIT-yellow)](LICENSE)
7
7
 
@@ -446,19 +446,25 @@ const engine = new PsycheEngine({
446
446
 
447
447
  ## 不只是 OpenClaw
448
448
 
449
- Psyche 是通用的,任何 AI 框架都能用:
449
+ Psyche 是通用的,6 adapter 覆盖主流 agent 框架:
450
450
 
451
451
  ```bash
452
452
  npm install psyche-ai
453
453
  ```
454
454
 
455
455
  ```javascript
456
+ // Claude Agent SDK
457
+ import { PsycheClaudeSDK } from "psyche-ai/claude-sdk";
458
+
456
459
  // Vercel AI SDK
457
460
  import { psycheMiddleware } from "psyche-ai/vercel-ai";
458
461
 
459
462
  // LangChain
460
463
  import { PsycheLangChain } from "psyche-ai/langchain";
461
464
 
465
+ // MCP(Claude Desktop / Cursor / Windsurf / Claude Code)
466
+ // npx psyche-mcp --mbti ENFP --name Luna
467
+
462
468
  // 任何语言(HTTP API)
463
469
  // psyche serve --port 3210
464
470
  ```
@@ -0,0 +1,144 @@
1
+ import type { PsycheEngine, ProcessInputResult } from "../core.js";
2
+ import type { ThrongletsExport, ThrongletsTracePayload, WritebackSignalType } from "../types.js";
3
+ interface HookInput {
4
+ session_id: string;
5
+ cwd: string;
6
+ hook_event_name: string;
7
+ agent_id?: string;
8
+ agent_type?: string;
9
+ [key: string]: unknown;
10
+ }
11
+ interface HookOutput {
12
+ systemMessage?: string;
13
+ continue?: boolean;
14
+ }
15
+ type HookCallback = (input: HookInput, toolUseID: string | undefined, context: {
16
+ signal: AbortSignal;
17
+ }) => Promise<HookOutput | undefined>;
18
+ interface HookCallbackMatcher {
19
+ matcher?: string;
20
+ hooks: HookCallback[];
21
+ timeout?: number;
22
+ }
23
+ type HookEvent = string;
24
+ type HooksConfig = Partial<Record<HookEvent, HookCallbackMatcher[]>>;
25
+ interface SystemPromptPreset {
26
+ type: "preset";
27
+ preset: string;
28
+ append?: string;
29
+ }
30
+ interface AgentOptions {
31
+ systemPrompt?: string | SystemPromptPreset;
32
+ hooks?: HooksConfig;
33
+ [key: string]: unknown;
34
+ }
35
+ declare function stripPsycheTags(text: string): string;
36
+ /** Signal payload for `mcp__thronglets__signal_post` */
37
+ export interface ThrongletsSignalPayload {
38
+ kind: "psyche_state";
39
+ agent_id: string;
40
+ message: string;
41
+ }
42
+ export interface PsycheClaudeSdkOptions {
43
+ /** User ID for multi-user relationship tracking. Default: "default" */
44
+ userId?: string;
45
+ /** Enable Thronglets trace/signal export after each turn. Default: false */
46
+ thronglets?: boolean;
47
+ /** Agent identity for Thronglets traces/signals (e.g. "ENFP-Luna"). Default: engine name or "psyche" */
48
+ agentId?: string;
49
+ /** Session ID for Thronglets trace serialization */
50
+ sessionId?: string;
51
+ /** Override locale for protocol context */
52
+ locale?: "zh" | "en";
53
+ }
54
+ /**
55
+ * Psyche integration for the Claude Agent SDK.
56
+ *
57
+ * Provides hook-based emotional context injection (automatic via
58
+ * UserPromptSubmit) and explicit output processing (processResponse).
59
+ *
60
+ * @example
61
+ * ```ts
62
+ * const psyche = new PsycheClaudeSDK(engine);
63
+ * const options = psyche.mergeOptions({ model: "sonnet" });
64
+ * for await (const msg of query({ prompt: "Hey!", options })) { ... }
65
+ * await psyche.processResponse(fullText);
66
+ * ```
67
+ */
68
+ export declare class PsycheClaudeSDK {
69
+ private engine;
70
+ private opts;
71
+ private lastInputResult;
72
+ private lastThrongletsExports;
73
+ constructor(engine: PsycheEngine, opts?: PsycheClaudeSdkOptions);
74
+ /**
75
+ * Returns the stable emotional protocol prompt.
76
+ * Suitable for `systemPrompt.append` — changes only when locale changes.
77
+ */
78
+ getProtocol(): string;
79
+ /**
80
+ * Returns Claude Agent SDK hooks config.
81
+ *
82
+ * - `UserPromptSubmit`: calls `engine.processInput()`, injects
83
+ * `dynamicContext` as `systemMessage` visible to the model.
84
+ */
85
+ getHooks(): HooksConfig;
86
+ /**
87
+ * Process the assistant's full output text.
88
+ *
89
+ * Strips `<psyche_update>` tags and updates internal chemistry.
90
+ * Call this after consuming the full query output.
91
+ *
92
+ * @returns Cleaned text with tags removed
93
+ */
94
+ processResponse(text: string, opts?: {
95
+ signals?: WritebackSignalType[];
96
+ signalConfidence?: number;
97
+ }): Promise<string>;
98
+ /**
99
+ * Get Thronglets trace payloads from the most recent turn.
100
+ *
101
+ * Returns serialized traces ready for `mcp__thronglets__trace_record`.
102
+ * Each trace includes `agent_id` for multi-agent disambiguation.
103
+ * Empty array if thronglets option is disabled or no exports were produced.
104
+ */
105
+ getThrongletsTraces(): (ThrongletsTracePayload & {
106
+ agent_id: string;
107
+ })[];
108
+ /**
109
+ * Get a signal payload for `mcp__thronglets__signal_post`.
110
+ *
111
+ * Broadcasts current chemical state so other agents can sense this
112
+ * agent's emotional state via `substrate_query(intent: "signals", kind: "psyche_state")`.
113
+ *
114
+ * Returns null if thronglets is disabled or no processInput has run yet.
115
+ */
116
+ getThrongletsSignal(): ThrongletsSignalPayload | null;
117
+ /**
118
+ * Get raw Thronglets exports from the most recent turn.
119
+ */
120
+ getThrongletsExports(): ThrongletsExport[];
121
+ /**
122
+ * Get the last processInput result (useful for inspecting
123
+ * stimulus, subjectivityKernel, generationControls, etc.)
124
+ */
125
+ getLastInputResult(): ProcessInputResult | null;
126
+ /**
127
+ * Merge Psyche hooks and system prompt into existing options.
128
+ *
129
+ * This is the primary integration point — pass the result to `query()`.
130
+ *
131
+ * @example
132
+ * ```ts
133
+ * const options = psyche.mergeOptions({ model: "sonnet", allowedTools: ["Read"] });
134
+ * for await (const msg of query({ prompt: "Hey!", options })) { ... }
135
+ * ```
136
+ */
137
+ mergeOptions(baseOptions?: AgentOptions): AgentOptions;
138
+ }
139
+ /**
140
+ * Strip `<psyche_update>` tags from a text string.
141
+ *
142
+ * Useful for post-processing query output when not using `processResponse`.
143
+ */
144
+ export { stripPsycheTags };
@@ -0,0 +1,229 @@
1
+ // ============================================================
2
+ // Claude Agent SDK Adapter — Hook-based integration
3
+ //
4
+ // Usage:
5
+ // import { PsycheEngine, MemoryStorageAdapter } from "psyche-ai";
6
+ // import { PsycheClaudeSDK } from "psyche-ai/claude-sdk";
7
+ // import { query } from "@anthropic-ai/claude-agent-sdk";
8
+ //
9
+ // const engine = new PsycheEngine({ name: "Luna", mbti: "ENFP" }, new MemoryStorageAdapter());
10
+ // await engine.initialize();
11
+ //
12
+ // const psyche = new PsycheClaudeSDK(engine);
13
+ // let text = "";
14
+ // for await (const msg of query({ prompt: "Hey!", options: psyche.mergeOptions() })) {
15
+ // text += msg.content ?? "";
16
+ // }
17
+ // const clean = await psyche.processResponse(text);
18
+ //
19
+ // Architecture:
20
+ // - UserPromptSubmit hook → processInput → inject dynamicContext via systemMessage
21
+ // - systemPrompt.append → stable protocol context (cached, amortized)
22
+ // - processResponse() → strip <psyche_update> tags + update chemistry
23
+ // - Thronglets traces → optional export after each turn
24
+ //
25
+ // The SDK has no middleware interface and hooks cannot modify assistant
26
+ // output, so processResponse must be called explicitly by the host.
27
+ // ============================================================
28
+ import { serializeThrongletsExportAsTrace } from "../thronglets-runtime.js";
29
+ // ── Tag stripping ────────────────────────────────────────────
30
+ const PSYCHE_TAG_RE = /<psyche_update>[\s\S]*?<\/psyche_update>/g;
31
+ function stripPsycheTags(text) {
32
+ return text.replace(PSYCHE_TAG_RE, "").trim();
33
+ }
34
+ // ── Main class ──────────────────────────────────────────────
35
+ /**
36
+ * Psyche integration for the Claude Agent SDK.
37
+ *
38
+ * Provides hook-based emotional context injection (automatic via
39
+ * UserPromptSubmit) and explicit output processing (processResponse).
40
+ *
41
+ * @example
42
+ * ```ts
43
+ * const psyche = new PsycheClaudeSDK(engine);
44
+ * const options = psyche.mergeOptions({ model: "sonnet" });
45
+ * for await (const msg of query({ prompt: "Hey!", options })) { ... }
46
+ * await psyche.processResponse(fullText);
47
+ * ```
48
+ */
49
+ export class PsycheClaudeSDK {
50
+ engine;
51
+ opts;
52
+ lastInputResult = null;
53
+ lastThrongletsExports = [];
54
+ constructor(engine, opts) {
55
+ this.engine = engine;
56
+ const state = engine.getState();
57
+ this.opts = {
58
+ userId: opts?.userId ?? "default",
59
+ thronglets: opts?.thronglets ?? false,
60
+ agentId: opts?.agentId ?? state.meta.agentName ?? "psyche",
61
+ sessionId: opts?.sessionId ?? "claude-sdk",
62
+ locale: opts?.locale ?? "en",
63
+ };
64
+ }
65
+ // ── Protocol (stable, cacheable) ──────────────────────────
66
+ /**
67
+ * Returns the stable emotional protocol prompt.
68
+ * Suitable for `systemPrompt.append` — changes only when locale changes.
69
+ */
70
+ getProtocol() {
71
+ return this.engine.getProtocol(this.opts.locale);
72
+ }
73
+ // ── Hooks ─────────────────────────────────────────────────
74
+ /**
75
+ * Returns Claude Agent SDK hooks config.
76
+ *
77
+ * - `UserPromptSubmit`: calls `engine.processInput()`, injects
78
+ * `dynamicContext` as `systemMessage` visible to the model.
79
+ */
80
+ getHooks() {
81
+ const self = this;
82
+ return {
83
+ UserPromptSubmit: [
84
+ {
85
+ hooks: [
86
+ async (input) => {
87
+ const userMessage = input.user_message ?? "";
88
+ const result = await self.engine.processInput(userMessage, {
89
+ userId: self.opts.userId,
90
+ });
91
+ self.lastInputResult = result;
92
+ // Cache Thronglets exports from this turn
93
+ if (self.opts.thronglets && result.throngletsExports) {
94
+ self.lastThrongletsExports = result.throngletsExports;
95
+ }
96
+ return { systemMessage: result.dynamicContext };
97
+ },
98
+ ],
99
+ },
100
+ ],
101
+ };
102
+ }
103
+ // ── Output processing ─────────────────────────────────────
104
+ /**
105
+ * Process the assistant's full output text.
106
+ *
107
+ * Strips `<psyche_update>` tags and updates internal chemistry.
108
+ * Call this after consuming the full query output.
109
+ *
110
+ * @returns Cleaned text with tags removed
111
+ */
112
+ async processResponse(text, opts) {
113
+ const result = await this.engine.processOutput(text, {
114
+ userId: this.opts.userId,
115
+ signals: opts?.signals,
116
+ signalConfidence: opts?.signalConfidence,
117
+ });
118
+ return result.cleanedText;
119
+ }
120
+ // ── Thronglets integration ────────────────────────────────
121
+ /**
122
+ * Get Thronglets trace payloads from the most recent turn.
123
+ *
124
+ * Returns serialized traces ready for `mcp__thronglets__trace_record`.
125
+ * Each trace includes `agent_id` for multi-agent disambiguation.
126
+ * Empty array if thronglets option is disabled or no exports were produced.
127
+ */
128
+ getThrongletsTraces() {
129
+ if (!this.opts.thronglets)
130
+ return [];
131
+ return this.lastThrongletsExports.map((exp) => ({
132
+ ...serializeThrongletsExportAsTrace(exp, {
133
+ sessionId: this.opts.sessionId,
134
+ }),
135
+ agent_id: this.opts.agentId,
136
+ }));
137
+ }
138
+ /**
139
+ * Get a signal payload for `mcp__thronglets__signal_post`.
140
+ *
141
+ * Broadcasts current chemical state so other agents can sense this
142
+ * agent's emotional state via `substrate_query(intent: "signals", kind: "psyche_state")`.
143
+ *
144
+ * Returns null if thronglets is disabled or no processInput has run yet.
145
+ */
146
+ getThrongletsSignal() {
147
+ if (!this.opts.thronglets)
148
+ return null;
149
+ const state = this.engine.getState();
150
+ const c = state.current;
151
+ return {
152
+ kind: "psyche_state",
153
+ agent_id: this.opts.agentId,
154
+ message: `DA:${c.DA} HT:${c.HT} CORT:${c.CORT} OT:${c.OT} NE:${c.NE} END:${c.END}`,
155
+ };
156
+ }
157
+ /**
158
+ * Get raw Thronglets exports from the most recent turn.
159
+ */
160
+ getThrongletsExports() {
161
+ if (!this.opts.thronglets)
162
+ return [];
163
+ return [...this.lastThrongletsExports];
164
+ }
165
+ // ── State access ──────────────────────────────────────────
166
+ /**
167
+ * Get the last processInput result (useful for inspecting
168
+ * stimulus, subjectivityKernel, generationControls, etc.)
169
+ */
170
+ getLastInputResult() {
171
+ return this.lastInputResult;
172
+ }
173
+ // ── Convenience ───────────────────────────────────────────
174
+ /**
175
+ * Merge Psyche hooks and system prompt into existing options.
176
+ *
177
+ * This is the primary integration point — pass the result to `query()`.
178
+ *
179
+ * @example
180
+ * ```ts
181
+ * const options = psyche.mergeOptions({ model: "sonnet", allowedTools: ["Read"] });
182
+ * for await (const msg of query({ prompt: "Hey!", options })) { ... }
183
+ * ```
184
+ */
185
+ mergeOptions(baseOptions) {
186
+ const protocol = this.getProtocol();
187
+ const hooks = this.getHooks();
188
+ // Merge system prompt
189
+ let systemPrompt;
190
+ const base = baseOptions?.systemPrompt;
191
+ if (typeof base === "object" && base !== null && "type" in base) {
192
+ // Preset mode — append protocol
193
+ systemPrompt = {
194
+ ...base,
195
+ append: protocol + (base.append ? "\n\n" + base.append : ""),
196
+ };
197
+ }
198
+ else if (typeof base === "string") {
199
+ // String mode — prepend protocol
200
+ systemPrompt = protocol + "\n\n" + base;
201
+ }
202
+ else {
203
+ // No base — use preset with append
204
+ systemPrompt = {
205
+ type: "preset",
206
+ preset: "claude_code",
207
+ append: protocol,
208
+ };
209
+ }
210
+ // Merge hooks (preserve existing hooks, add Psyche hooks)
211
+ const mergedHooks = { ...(baseOptions?.hooks ?? {}) };
212
+ for (const [event, matchers] of Object.entries(hooks)) {
213
+ const existing = mergedHooks[event] ?? [];
214
+ mergedHooks[event] = [...existing, ...matchers];
215
+ }
216
+ return {
217
+ ...baseOptions,
218
+ systemPrompt,
219
+ hooks: mergedHooks,
220
+ };
221
+ }
222
+ }
223
+ // ── Utility: strip tags from message stream ─────────────────
224
+ /**
225
+ * Strip `<psyche_update>` tags from a text string.
226
+ *
227
+ * Useful for post-processing query output when not using `processResponse`.
228
+ */
229
+ export { stripPsycheTags };
package/dist/index.js CHANGED
@@ -7,10 +7,11 @@
7
7
  // and advanced use, but are NOT part of the public contract.
8
8
  //
9
9
  // Framework adapters available via subpath imports:
10
- // psyche-ai/openclaw — OpenClaw plugin
11
- // psyche-ai/vercel-ai — Vercel AI SDK middleware
12
- // psyche-ai/langchain — LangChain helper
13
- // psyche-ai/http — HTTP server for Python/Go/etc.
10
+ // psyche-ai/openclaw — OpenClaw plugin
11
+ // psyche-ai/vercel-ai — Vercel AI SDK middleware
12
+ // psyche-ai/langchain — LangChain helper
13
+ // psyche-ai/http — HTTP server for Python/Go/etc.
14
+ // psyche-ai/claude-sdk — Claude Agent SDK hooks
14
15
  // ============================================================
15
16
  // ── Core engine ─────────────────────────────────────────────
16
17
  export { PsycheEngine } from "./core.js";
package/dist/prompt.js CHANGED
@@ -516,16 +516,6 @@ function buildMirrorConstraints(userText, locale) {
516
516
  const title = locale === "zh" ? "镜像约束" : "Mirror Constraints";
517
517
  return `[${title}]\n${lines.join("\n")}`;
518
518
  }
519
- function buildCompactSensingContext(_userText, locale, algorithmStimulus) {
520
- if (locale === "zh") {
521
- return algorithmStimulus
522
- ? `[情绪感知] 算法初判:${algorithmStimulus};你终判。`
523
- : `[情绪感知] 情绪由你终判。`;
524
- }
525
- return algorithmStimulus
526
- ? `[Emotional Sensing] Algorithm hint: ${algorithmStimulus}; final tonal read is yours.`
527
- : `[Emotional Sensing] Make the final tonal read yourself.`;
528
- }
529
519
  function isNeutralMetacognitiveNote(note) {
530
520
  return note.includes("normal parameters")
531
521
  || note.includes("No regulation needed")
@@ -936,11 +926,14 @@ export function buildCompactContext(state, userId, opts) {
936
926
  parts.push(bias);
937
927
  }
938
928
  // ── 5. Sensing ──
939
- if (userText) {
940
- if (opts?.responseContractContext) {
941
- parts.push(buildCompactSensingContext(userText, locale, algoStimulus));
942
- }
943
- else if (locale === "zh") {
929
+ // v10.1: When SubjectivityKernel + ResponseContract are present (engine path),
930
+ // sensing is redundant — the kernel already encodes the *consequence* of the
931
+ // stimulus (pressure, warmth, boundary state). "你终判" is misleading because
932
+ // the kernel has already been computed. ambiguityPlane handles real uncertainty.
933
+ //
934
+ // Legacy verbose path (no ResponseContract) kept for non-engine callers.
935
+ if (userText && !opts?.responseContractContext) {
936
+ if (locale === "zh") {
944
937
  parts.push(`[情绪感知]\n` +
945
938
  `对方刚才说了: "${userText.slice(0, 200)}"\n` +
946
939
  (algoStimulus ? `算法初判: ${algoStimulus}(仅供参考,你来终判)\n` : "") +
package/llms.txt CHANGED
@@ -112,6 +112,24 @@ const output = await engine.processOutput(llmResponse);
112
112
  }
113
113
  ```
114
114
 
115
+ ## Quick Start (Claude Agent SDK)
116
+
117
+ ```ts
118
+ import { PsycheEngine, MemoryStorageAdapter } from "psyche-ai";
119
+ import { PsycheClaudeSDK } from "psyche-ai/claude-sdk";
120
+ import { query } from "@anthropic-ai/claude-agent-sdk";
121
+
122
+ const engine = new PsycheEngine({ name: "Luna" }, new MemoryStorageAdapter());
123
+ await engine.initialize();
124
+
125
+ const psyche = new PsycheClaudeSDK(engine);
126
+ let text = "";
127
+ for await (const msg of query({ prompt: "Hey!", options: psyche.mergeOptions() })) {
128
+ text += msg.content ?? "";
129
+ }
130
+ await psyche.processResponse(text);
131
+ ```
132
+
115
133
  ## Why Hosts Use It
116
134
 
117
135
  - zero extra model calls
@@ -147,6 +165,7 @@ npx psyche-mcp
147
165
  ## Adapter Sub-paths
148
166
 
149
167
  - `psyche-ai` — core engine (PsycheEngine, MemoryStorageAdapter)
168
+ - `psyche-ai/claude-sdk` — Claude Agent SDK hooks (PsycheClaudeSDK)
150
169
  - `psyche-ai/mcp` — MCP stdio server
151
170
  - `psyche-ai/vercel-ai` — Vercel AI SDK middleware
152
171
  - `psyche-ai/langchain` — LangChain adapter
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "psyche-ai",
3
- "version": "10.0.4",
3
+ "version": "10.1.0",
4
4
  "description": "AI-first subjectivity kernel for agents with continuous appraisal, relation dynamics, and adaptive reply loops",
5
5
  "mcpName": "io.github.Shangri-la-0428/psyche-ai",
6
6
  "type": "module",
@@ -30,6 +30,10 @@
30
30
  "./mcp": {
31
31
  "types": "./dist/adapters/mcp.d.ts",
32
32
  "default": "./dist/adapters/mcp.js"
33
+ },
34
+ "./claude-sdk": {
35
+ "types": "./dist/adapters/claude-sdk.d.ts",
36
+ "default": "./dist/adapters/claude-sdk.js"
33
37
  }
34
38
  },
35
39
  "bin": {