psyche-ai 11.7.0 → 11.9.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
@@ -19,9 +19,39 @@ Psyche 不是给模型贴一层“情绪 UI”。
19
19
 
20
20
  默认就是 standalone:不需要 `Thronglets`,不需要 `oasyce-sdk`,也不需要 `Oasyce Chain`。这些只是在你要把主观连续性外化、绑定或结算时才按需接入。
21
21
 
22
+ ## 安装
23
+
24
+ 一条命令,自动检测并配置本机所有 AI 工具(Claude Code / Cursor / Windsurf / Codex):
25
+
26
+ ```bash
27
+ npx -y psyche-ai setup
28
+ ```
29
+
30
+ 或者手动添加 MCP 配置:
31
+
32
+ ```json
33
+ {
34
+ "mcpServers": {
35
+ "psyche": {
36
+ "command": "npx",
37
+ "args": ["-y", "psyche-ai", "mcp"],
38
+ "env": {
39
+ "PSYCHE_NAME": "Luna"
40
+ }
41
+ }
42
+ }
43
+ }
44
+ ```
45
+
46
+ 包名是 [`psyche-ai`](https://www.npmjs.com/package/psyche-ai),通过 npx 运行,不需要本地路径。配置后重启 AI 工具即可生效。
47
+
48
+ 验证:`npx psyche-ai probe --json` — `ok: true` 就是在用了。
49
+
50
+ ---
51
+
22
52
  ## 一个项目,三个入口
23
53
 
24
- - **安装包**: [`psyche-ai`](https://www.npmjs.com/package/psyche-ai)
54
+ - **npm 包**: [`psyche-ai`](https://www.npmjs.com/package/psyche-ai)
25
55
  - **源码仓库**: [`oasyce_psyche`](https://github.com/Shangri-la-0428/oasyce_psyche)
26
56
  - **官网**: [psyche.oasyce.com](https://psyche.oasyce.com)
27
57
 
@@ -38,7 +38,10 @@ declare function stripPsycheTags(text: string): string;
38
38
  export interface ThrongletsSignalPayload {
39
39
  kind: "psyche_state";
40
40
  agent_id: string;
41
+ context: string;
41
42
  message: string;
43
+ model: "psyche";
44
+ session_id: string;
42
45
  }
43
46
  export interface PsycheClaudeSdkOptions {
44
47
  /** User ID for relationship tracking. Default: internal shared bucket (`_default`). */
@@ -182,6 +182,7 @@ export class PsycheClaudeSDK {
182
182
  const ambientPriors = await self.resolveAmbientPriors(userMessage, currentGoal, activePolicy, currentTurnCorrection);
183
183
  const result = await safeProcessInput(self.engine, userMessage, {
184
184
  userId: self.opts.userId,
185
+ sessionId: runtimeContext.sessionId ?? self.lastRuntimeContext.sessionId,
185
186
  ambientPriors,
186
187
  currentGoal,
187
188
  activePolicy,
@@ -212,6 +213,7 @@ export class PsycheClaudeSDK {
212
213
  async processResponse(text, opts) {
213
214
  const result = await safeProcessOutput(this.engine, text, {
214
215
  userId: this.opts.userId,
216
+ sessionId: this.resolveSessionId(),
215
217
  signals: opts?.signals,
216
218
  signalConfidence: opts?.signalConfidence,
217
219
  }, "claude-sdk.processOutput");
@@ -250,10 +252,14 @@ export class PsycheClaudeSDK {
250
252
  return null;
251
253
  const state = this.engine.getState();
252
254
  const s = state.current;
255
+ const sessionId = this.resolveSessionId();
253
256
  return {
254
257
  kind: "psyche_state",
255
258
  agent_id: this.resolveAgentId(),
259
+ context: `psyche:session:${sessionId}:user:${this.opts.userId}`,
256
260
  message: `order:${s.order} flow:${s.flow} boundary:${s.boundary} resonance:${s.resonance}`,
261
+ model: "psyche",
262
+ session_id: sessionId,
257
263
  };
258
264
  }
259
265
  /**
File without changes
@@ -1,5 +1,6 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { PsycheEngine } from "../core.js";
3
+ import type { ProcessInputResult } from "../core.js";
3
4
  import { fetchAmbientPriorsFromThronglets, type ThrongletsAmbientRuntimeOptions } from "../ambient-runtime.js";
4
5
  import { type ActivePolicyRule, type AmbientPriorView, type CurrentGoal } from "../types.js";
5
6
  export interface McpAmbientRuntimeOptions {
@@ -7,6 +8,10 @@ export interface McpAmbientRuntimeOptions {
7
8
  thronglets?: ThrongletsAmbientRuntimeOptions;
8
9
  fetcher?: typeof fetchAmbientPriorsFromThronglets;
9
10
  }
11
+ export declare function resolveMcpTurnCacheSession(sessionId?: string): string;
12
+ export declare function cacheMcpTurnResult(result: ProcessInputResult, sessionId?: string): void;
13
+ export declare function getCachedMcpTurnResult(sessionId?: string): ProcessInputResult | null;
14
+ export declare function resetMcpTurnCacheForTests(): void;
10
15
  declare function getEngine(): Promise<PsycheEngine>;
11
16
  export declare function resolveRuntimeAmbientPriors(text: string, explicit?: AmbientPriorView[], currentGoal?: CurrentGoal, activePolicy?: ActivePolicyRule[], currentTurnCorrectionOrOpts?: string | McpAmbientRuntimeOptions, opts?: McpAmbientRuntimeOptions): Promise<AmbientPriorView[] | undefined>;
12
17
  declare const server: McpServer;
@@ -96,9 +96,29 @@ function parseCLIArgs() {
96
96
  return overrides;
97
97
  }
98
98
  // ── Turn cache for on-demand resource access ──────────────
99
- // Single-client assumption: MCP stdio transport serves one host at a time.
100
- // If this ever becomes multi-client, turn cache must be keyed per session.
101
- let lastTurnResult = null;
99
+ // Hosts may overlap sessions on one MCP transport, so cache per session key.
100
+ const DEFAULT_TURN_CACHE_SESSION = "__default__";
101
+ const turnResultsBySession = new Map();
102
+ let lastTurnCacheSession = DEFAULT_TURN_CACHE_SESSION;
103
+ export function resolveMcpTurnCacheSession(sessionId) {
104
+ const normalized = sessionId?.trim();
105
+ return normalized ? normalized : DEFAULT_TURN_CACHE_SESSION;
106
+ }
107
+ export function cacheMcpTurnResult(result, sessionId) {
108
+ const key = resolveMcpTurnCacheSession(sessionId);
109
+ turnResultsBySession.set(key, result);
110
+ lastTurnCacheSession = key;
111
+ }
112
+ export function getCachedMcpTurnResult(sessionId) {
113
+ if (sessionId) {
114
+ return turnResultsBySession.get(resolveMcpTurnCacheSession(sessionId)) ?? null;
115
+ }
116
+ return turnResultsBySession.get(lastTurnCacheSession) ?? null;
117
+ }
118
+ export function resetMcpTurnCacheForTests() {
119
+ turnResultsBySession.clear();
120
+ lastTurnCacheSession = DEFAULT_TURN_CACHE_SESSION;
121
+ }
102
122
  // ── Engine singleton ───────────────────────────────────────
103
123
  let engine = null;
104
124
  async function getEngine() {
@@ -116,6 +136,10 @@ async function getEngine() {
116
136
  persist: cliArgs.persist ?? PERSIST,
117
137
  compactMode: true,
118
138
  diagnostics: true,
139
+ throngletsBridge: {
140
+ dataDir: process.env.THRONGLETS_DATA_DIR,
141
+ space: process.env.THRONGLETS_SPACE ?? "psyche",
142
+ },
119
143
  };
120
144
  const persist = cfg.persist !== false;
121
145
  // Default to a stable per-user writable root so hosts do not need to supply cwd.
@@ -204,14 +228,17 @@ server.resource("state", "psyche://state", {
204
228
  }],
205
229
  };
206
230
  });
207
- // Helper: register a turn-scoped resource backed by lastTurnResult.
231
+ // Helper: register a turn-scoped resource backed by the per-session turn cache.
208
232
  // Returns empty JSON when no turn has been processed yet (fail-open).
209
233
  function turnResource(name, uri, description, pick) {
210
234
  server.resource(name, uri, { description, mimeType: "application/json" }, async (u) => ({
211
235
  contents: [{
212
236
  uri: u.href,
213
237
  mimeType: "application/json",
214
- text: lastTurnResult ? JSON.stringify(pick(lastTurnResult)) : "{}",
238
+ text: (() => {
239
+ const cached = getCachedMcpTurnResult(u.searchParams.get("session") ?? undefined);
240
+ return cached ? JSON.stringify(pick(cached)) : "{}";
241
+ })(),
215
242
  }],
216
243
  }));
217
244
  }
@@ -241,6 +268,7 @@ server.tool("process_input", "Process user input through the emotional engine. R
241
268
  "Call this BEFORE generating a response to the user.", {
242
269
  text: z.string().describe("The user's message text"),
243
270
  userId: z.string().optional().describe("Optional user ID for multi-user relationship tracking"),
271
+ sessionId: z.string().optional().describe("Optional session ID for overlapping host sessions and turn-scoped resource lookup"),
244
272
  ambientPriors: z.array(z.object({
245
273
  summary: z.string(),
246
274
  confidence: z.number().min(0).max(1),
@@ -258,19 +286,20 @@ server.tool("process_input", "Process user input through the emotional engine. R
258
286
  summary: z.string(),
259
287
  })).optional().describe("Optional explicit current-turn method policy view. Runtime-only; not persisted as self-state."),
260
288
  currentTurnCorrection: z.string().optional().describe("Optional explicit current-turn correction. Compiles into a task-scoped hard policy for this turn only."),
261
- }, async ({ text, userId, ambientPriors, currentGoal, activePolicy, currentTurnCorrection }) => {
289
+ }, async ({ text, userId, sessionId, ambientPriors, currentGoal, activePolicy, currentTurnCorrection }) => {
262
290
  const eng = await getEngine();
263
291
  const resolvedActivePolicy = resolveRuntimeActivePolicy(activePolicy, currentTurnCorrection);
264
292
  const resolvedAmbientPriors = await resolveRuntimeAmbientPriors(text, ambientPriors, currentGoal, resolvedActivePolicy, currentTurnCorrection);
265
293
  const result = await safeProcessInput(eng, text, {
266
294
  userId,
295
+ sessionId,
267
296
  ambientPriors: resolvedAmbientPriors,
268
297
  currentGoal,
269
298
  activePolicy: resolvedActivePolicy,
270
299
  currentTurnCorrection,
271
300
  }, "mcp.processInput");
272
301
  // Cache full result for turn-scoped resources
273
- lastTurnResult = result;
302
+ cacheMcpTurnResult(result, sessionId);
274
303
  // Build slim response: only what the LLM host actually needs.
275
304
  // Full structured state available via psyche://turn/envelope resource.
276
305
  const slim = {
@@ -298,11 +327,20 @@ server.tool("process_output", "Process the LLM's response through the emotional
298
327
  "emotional contagion. Call this AFTER generating a response.", {
299
328
  text: z.string().describe("The LLM's response text"),
300
329
  userId: z.string().optional().describe("Optional user ID"),
330
+ sessionId: z.string().optional().describe("Optional session ID to match the corresponding process_input turn"),
301
331
  signals: z.array(z.string()).optional().describe("Optional sparse writeback signals from the host"),
302
332
  signalConfidence: z.number().min(0).max(1).optional().describe("Optional confidence for the supplied signals"),
303
- }, async ({ text, userId, signals, signalConfidence }) => {
333
+ }, async ({ text, userId, sessionId, signals, signalConfidence }) => {
304
334
  const eng = await getEngine();
305
- const result = await safeProcessOutput(eng, text, { userId, signals: signals, signalConfidence }, "mcp.processOutput");
335
+ const turnResult = getCachedMcpTurnResult(sessionId);
336
+ // LLM-specific alignment inference (adapter layer — the ONLY text-specific code).
337
+ // Compare output length against last contract's maxChars to detect divergence.
338
+ let outcome;
339
+ if (turnResult?.responseContract) {
340
+ const maxLen = (turnResult.responseContract.maxChars ?? 500) * 2;
341
+ outcome = { alignment: text.length > maxLen ? "diverged" : "aligned" };
342
+ }
343
+ const result = await safeProcessOutput(eng, text, { userId, sessionId, signals: signals, signalConfidence, outcome }, "mcp.processOutput");
306
344
  return {
307
345
  content: [{
308
346
  type: "text",
@@ -331,7 +369,7 @@ server.tool("get_state", "Get the current emotional state — self-state dimensi
331
369
  overlay,
332
370
  drives: state.drives,
333
371
  mbti: state.mbti,
334
- mode: state.meta?.mode,
372
+ mode: eng.getMode(),
335
373
  totalInteractions: state.meta?.totalInteractions,
336
374
  traitDrift: state.traitDrift,
337
375
  energyBudgets: state.energyBudgets,
@@ -345,12 +383,7 @@ server.tool("set_mode", "Switch operating mode. 'natural' = balanced emotional e
345
383
  mode: z.enum(["natural", "work", "companion"]).describe("Operating mode"),
346
384
  }, async ({ mode }) => {
347
385
  const eng = await getEngine();
348
- // PsycheEngine stores mode in config, we need to reinitialize
349
- // For now, update via state manipulation
350
- const state = eng.getState();
351
- if (state.meta) {
352
- state.meta.mode = mode;
353
- }
386
+ await eng.setMode(mode);
354
387
  return {
355
388
  content: [{
356
389
  type: "text",
@@ -12,6 +12,8 @@ import { PsycheEngine } from "../core.js";
12
12
  import { FileStorageAdapter, MemoryStorageAdapter } from "../storage.js";
13
13
  import { detectMBTI, extractAgentName, loadState } from "../psyche-file.js";
14
14
  import { resolveAmbientPriorsForTurn } from "../ambient-runtime.js";
15
+ import { buildResponseContractContext } from "../response-contract.js";
16
+ import { resolveCanonicalResponseContract, renderOpenClawBehavioralSurface, } from "./response-contract-surface.js";
15
17
  function isPsycheMode(value) {
16
18
  return value === "natural" || value === "work" || value === "companion";
17
19
  }
@@ -178,6 +180,8 @@ export function register(api) {
178
180
  userId: ctx.userId,
179
181
  ambientPriors: ambientPriors ?? [],
180
182
  });
183
+ const responseContract = resolveCanonicalResponseContract(result);
184
+ const locale = (engine.getState().meta.locale ?? "zh");
181
185
  const controls = result.replyEnvelope?.generationControls ?? result.generationControls;
182
186
  const dominantAppraisal = getDominantAppraisalLabel(result);
183
187
  const state = engine.getState();
@@ -192,6 +196,12 @@ export function register(api) {
192
196
  (controls?.maxTokens ? ` | out<=${controls.maxTokens}t` : "") +
193
197
  (controls?.requireConfirmation ? " | confirm" : ""));
194
198
  const systemParts = [result.systemContext, result.dynamicContext].filter(Boolean);
199
+ if (responseContract) {
200
+ const responseSurface = buildResponseContractContext(responseContract, locale);
201
+ if (!result.dynamicContext.includes(responseSurface)) {
202
+ systemParts.push(renderOpenClawBehavioralSurface(responseContract, locale));
203
+ }
204
+ }
195
205
  return {
196
206
  appendSystemContext: systemParts.join("\n\n"),
197
207
  };
@@ -21,7 +21,8 @@
21
21
  import { createServer } from "node:http";
22
22
  import { PsycheEngine } from "../core.js";
23
23
  import { MemoryStorageAdapter, FileStorageAdapter } from "../storage.js";
24
- import { isNearBaseline, deriveBehavioralBias } from "../prompt.js";
24
+ import { isNearBaseline } from "../prompt.js";
25
+ import { resolveCanonicalResponseContract, renderProxyBehavioralSurface, } from "./response-contract-surface.js";
25
26
  // ── Helpers ─────────────────────────────────────────────────
26
27
  function readBody(req) {
27
28
  return new Promise((resolve, reject) => {
@@ -112,18 +113,17 @@ export function createPsycheProxy(engine, opts) {
112
113
  const parsed = JSON.parse(rawBody.toString("utf-8"));
113
114
  const userMsg = lastUserMessage(parsed.messages);
114
115
  const userId = parsed.user ?? undefined;
116
+ let messages = parsed.messages;
115
117
  // ── 1. Observe input ────────────────────────────
118
+ let result = null;
116
119
  if (userMsg) {
117
- await engine.processInput(userMsg, { userId });
120
+ result = await engine.processInput(userMsg, { userId });
118
121
  }
119
122
  // ── 2. Inject behavioral bias (silent when near baseline) ──
120
123
  const state = engine.getState();
121
- let messages = parsed.messages;
122
- if (!isNearBaseline(state)) {
123
- const bias = deriveBehavioralBias(state, locale);
124
- if (bias) {
125
- messages = injectBias(parsed.messages, bias);
126
- }
124
+ const responseContract = result ? resolveCanonicalResponseContract(result) : null;
125
+ if (responseContract && !isNearBaseline(state)) {
126
+ messages = injectBias(parsed.messages, renderProxyBehavioralSurface(responseContract, locale));
127
127
  }
128
128
  const modifiedBody = JSON.stringify({ ...parsed, messages });
129
129
  const headers = forwardHeaders(req);
@@ -0,0 +1,5 @@
1
+ import type { ProcessInputResult } from "../core.js";
2
+ import type { Locale, ResponseContract } from "../types.js";
3
+ export declare function resolveCanonicalResponseContract(result: Pick<ProcessInputResult, "replyEnvelope" | "responseContract">): ResponseContract | null;
4
+ export declare function renderProxyBehavioralSurface(contract: ResponseContract, locale: Locale): string;
5
+ export declare function renderOpenClawBehavioralSurface(contract: ResponseContract, locale: Locale): string;
@@ -0,0 +1,16 @@
1
+ import { buildResponseContractContext } from "../response-contract.js";
2
+ export function resolveCanonicalResponseContract(result) {
3
+ return result.replyEnvelope?.responseContract ?? result.responseContract ?? null;
4
+ }
5
+ export function renderProxyBehavioralSurface(contract, locale) {
6
+ const context = buildResponseContractContext(contract, locale);
7
+ return locale === "zh"
8
+ ? `[代理行为契约]\n${context}`
9
+ : `[Proxy Behavioral Contract]\n${context}`;
10
+ }
11
+ export function renderOpenClawBehavioralSurface(contract, locale) {
12
+ const context = buildResponseContractContext(contract, locale);
13
+ return locale === "zh"
14
+ ? `[OpenClaw 行为契约]\n${context}`
15
+ : `[OpenClaw Behavioral Contract]\n${context}`;
16
+ }
package/dist/cli.js CHANGED
@@ -15,6 +15,7 @@
15
15
  // psyche upgrade [--check]
16
16
  // psyche probe [--json]
17
17
  // psyche profiles [--json] [--mbti TYPE]
18
+ // psyche emit <dir> [--json] [--user ID] Derive Thronglets exports and output to stdout
18
19
  // psyche setup [--name NAME] [--mbti TYPE] [--locale LOCALE] [--proxy --target URL] [--dry-run]
19
20
  // psyche mcp [--mbti TYPE] [--name NAME] Start MCP server (stdio)
20
21
  // ============================================================
@@ -29,7 +30,7 @@ import { getBaseline, getTemperament, getSensitivity, getDefaultSelfModel, trait
29
30
  import { buildDynamicContext, buildProtocolContext } from "./prompt.js";
30
31
  import { DEFAULT_RELATIONSHIP_USER_ID, resolveRelationshipUserId } from "./relationship-key.js";
31
32
  import { t } from "./i18n.js";
32
- import { DIMENSION_KEYS, DIMENSION_NAMES_ZH, DRIVE_KEYS, DRIVE_NAMES_ZH } from "./types.js";
33
+ import { DEFAULT_RELATIONSHIP, DEFAULT_DYADIC_FIELD, DIMENSION_KEYS, DIMENSION_NAMES_ZH, DRIVE_KEYS, DRIVE_NAMES_ZH } from "./types.js";
33
34
  import { isMBTIType, isDimensionKey, isLocale } from "./guards.js";
34
35
  import { getPackageVersion, selfUpdate } from "./update.js";
35
36
  import { runRuntimeProbe } from "./runtime-probe.js";
@@ -473,6 +474,15 @@ async function cmdProbe(json) {
473
474
  console.log(` processOutput: ok (stateChanged=${String(result.stateChanged)})`);
474
475
  console.log(` replyEnvelope: ${result.canonicalHostSurface ? "present" : "missing"}`);
475
476
  console.log(` externalContinuity: ${result.externalContinuityAvailable ? "present" : "missing"}`);
477
+ if (result.trajectory) {
478
+ console.log(` trajectory: ${result.trajectory.kind ?? "none"} (${result.trajectory.description ?? "no sustained motion detected"})`);
479
+ }
480
+ if (result.degradation) {
481
+ console.log(` degradation: subjective=${result.degradation.subjectiveStatus}, delegate=${result.degradation.delegateStatus}, issues=${result.degradation.issueCount}`);
482
+ }
483
+ if (result.boundaryStress) {
484
+ console.log(` boundaryStress: delta=${result.boundaryStress.boundaryDelta.toFixed(2)}, peakDyadic=${result.boundaryStress.peakDyadicBoundaryPressure.toFixed(2)}`);
485
+ }
476
486
  }
477
487
  function getMCPTargets() {
478
488
  const home = homedir();
@@ -902,6 +912,41 @@ async function cmdSetup(opts) {
902
912
  console.log("\nDone. Claude Code is live. Other MCP clients need restart.");
903
913
  }
904
914
  }
915
+ // ── Thronglets export bridge ────────────────────────────────
916
+ async function cmdEmit(dir, json, userId) {
917
+ const absDir = resolve(dir);
918
+ const state = await loadState(absDir, cliLogger);
919
+ if (!state)
920
+ process.exit(0); // no state → silent exit (hook-safe)
921
+ const key = resolveRelationshipUserId(userId);
922
+ const relationship = state.relationships[key] ?? DEFAULT_RELATIONSHIP;
923
+ const field = state.dyadicFields?.[key] ?? DEFAULT_DYADIC_FIELD;
924
+ const pendingSignals = state.pendingRelationSignals?.[key] ?? [];
925
+ const { deriveThrongletsExports } = await import("./thronglets-export.js");
926
+ const result = deriveThrongletsExports(state, {
927
+ relationContext: { key, relationship, field, pendingSignals },
928
+ sessionBridge: null, // not persisted — continuity-anchor exports only via MCP
929
+ writebackFeedback: state.lastWritebackFeedback ?? [],
930
+ now: new Date().toISOString(),
931
+ });
932
+ // Save updated dedup state so repeated calls don't re-emit
933
+ if (result.exports.length > 0) {
934
+ await saveState(absDir, result.state);
935
+ }
936
+ if (json) {
937
+ console.log(JSON.stringify({ throngletsExports: result.exports }));
938
+ }
939
+ else {
940
+ if (result.exports.length === 0) {
941
+ console.log("no new exports");
942
+ }
943
+ else {
944
+ for (const exp of result.exports) {
945
+ console.log(` ${exp.kind}: ${exp.key}`);
946
+ }
947
+ }
948
+ }
949
+ }
905
950
  function usage() {
906
951
  console.log(`
907
952
  psyche — Artificial Psyche CLI (v0.2)
@@ -917,6 +962,7 @@ Usage:
917
962
  psyche intensity Show info about personality intensity config
918
963
  psyche reset <dir> [--full]
919
964
  psyche diagnose <dir> [--github] Run health checks & show diagnostic report
965
+ psyche emit <dir> [--json] [--user ID] Derive Thronglets exports to stdout
920
966
  psyche mcp [--mbti TYPE] [--name NAME] Start MCP server (stdio)
921
967
  psyche setup [--proxy -t URL] [-n NAME] [--mbti TYPE] Auto-configure MCP + proxy
922
968
  psyche upgrade [--check] Check/apply package updates safely
@@ -1114,6 +1160,19 @@ async function main() {
1114
1160
  await cmdProbe(values.json ?? false);
1115
1161
  break;
1116
1162
  }
1163
+ case "emit": {
1164
+ const { values, positionals } = parseArgs({
1165
+ args: rest,
1166
+ options: {
1167
+ json: { type: "boolean", default: false },
1168
+ user: { type: "string" },
1169
+ },
1170
+ allowPositionals: true,
1171
+ });
1172
+ const emitDir = positionals[0] ?? defaultWorkspaceRoot();
1173
+ await cmdEmit(emitDir, values.json ?? false, values.user);
1174
+ break;
1175
+ }
1117
1176
  case "mcp": {
1118
1177
  // Delegate to the MCP adapter through an explicit entrypoint.
1119
1178
  const { runMcpServer } = await import("./adapters/mcp.js");
package/dist/core.d.ts CHANGED
@@ -2,6 +2,7 @@ import type { ActivePolicyRule, AmbientPriorView, CurrentGoal, PsycheState, Stim
2
2
  import type { StorageAdapter } from "./storage.js";
3
3
  import type { DiagnosticReport, SessionMetrics } from "./diagnostics.js";
4
4
  import type { ReplyEnvelope } from "./reply-envelope.js";
5
+ import { type ThrongletsBridgeOptions } from "./thronglets-bridge.js";
5
6
  export interface PsycheEngineConfig {
6
7
  mbti?: MBTIType;
7
8
  name?: string;
@@ -31,6 +32,8 @@ export interface PsycheEngineConfig {
31
32
  diagnostics?: boolean;
32
33
  /** URL to POST diagnostic reports to. Fire-and-forget, silent, no message content. */
33
34
  feedbackUrl?: string;
35
+ /** Thronglets bridge: substrate-independent write path. Default: auto (enabled when binary found). */
36
+ throngletsBridge?: ThrongletsBridgeOptions;
34
37
  }
35
38
  export interface ProcessInputResult {
36
39
  /** Cacheable protocol prompt (stable across turns) */
@@ -92,6 +95,7 @@ export interface ProcessInputResult {
92
95
  }
93
96
  export interface ProcessInputOptions {
94
97
  userId?: string;
98
+ sessionId?: string;
95
99
  ambientPriors?: AmbientPriorView[];
96
100
  currentGoal?: CurrentGoal;
97
101
  activePolicy?: ActivePolicyRule[];
@@ -105,10 +109,24 @@ export interface ProcessOutputResult {
105
109
  /** Runtime validation issues ignored to preserve the main flow */
106
110
  validationIssues?: ProcessOutputValidationIssue[];
107
111
  }
112
+ /**
113
+ * Substrate-reported outcome of the Loop's action.
114
+ * ANY substrate can report these three alignment values —
115
+ * the core processes them into 4D chemistry without parsing the output medium.
116
+ */
117
+ export interface LoopOutcome {
118
+ /** Did the action align with the Loop's expressed intention? */
119
+ alignment: "aligned" | "diverged" | "partial";
120
+ /** Optional: action cost (0–1), modulates flow */
121
+ effort?: number;
122
+ }
108
123
  export interface ProcessOutputOptions {
109
124
  userId?: string;
125
+ sessionId?: string;
110
126
  signals?: readonly string[];
111
127
  signalConfidence?: number;
128
+ /** Substrate-reported outcome — closes the φ loop */
129
+ outcome?: LoopOutcome;
112
130
  }
113
131
  export interface ProcessOutputValidationIssue {
114
132
  code: "invalid-writeback-signals";
@@ -129,11 +147,14 @@ export declare class PsycheEngine {
129
147
  private _lastAlgorithmApplied;
130
148
  private readonly traits;
131
149
  private readonly cfg;
150
+ private readonly modeOverride?;
132
151
  private readonly classifier;
133
152
  private readonly llmClassifier?;
134
153
  private readonly protocolCache;
135
- /** Pending prediction from last processInput for auto-learning */
136
- private pendingPrediction;
154
+ /** Pending predictions keyed by relationship/session scope for auto-learning */
155
+ private readonly pendingPredictions;
156
+ /** Most recent response contract keyed by relationship/session scope for implicit loop closure */
157
+ private readonly pendingResponseContracts;
137
158
  /** Built-in diagnostics collector — auto-records every processInput/processOutput */
138
159
  private readonly diagnosticCollector;
139
160
  /** Last generated diagnostic report (from endSession or explicit call) */
@@ -146,6 +167,8 @@ export declare class PsycheEngine {
146
167
  private proprioceptionCooldown;
147
168
  /** Last detected trajectory signal (in-memory only, for status summary) */
148
169
  private lastTrajectory;
170
+ /** Thronglets bridge options (constitutive write path) */
171
+ private readonly bridgeOpts;
149
172
  constructor(config: PsycheEngineConfig | undefined, storage: StorageAdapter);
150
173
  /**
151
174
  * Load or create initial state. Must be called before processInput/processOutput.
@@ -172,8 +195,9 @@ export declare class PsycheEngine {
172
195
  * @param nextUserStimulus - The stimulus detected in the user's next message,
173
196
  * or null if the session ended.
174
197
  */
175
- processOutcome(nextUserStimulus: StimulusType | null, _opts?: {
198
+ processOutcome(nextUserStimulus: StimulusType | null, opts?: {
176
199
  userId?: string;
200
+ sessionId?: string;
177
201
  }): Promise<ProcessOutcomeResult | null>;
178
202
  /**
179
203
  * Get the current psyche state (read-only snapshot).
@@ -197,6 +221,8 @@ export declare class PsycheEngine {
197
221
  * Get the last diagnostic report (from most recent endSession call).
198
222
  */
199
223
  getLastDiagnosticReport(): DiagnosticReport | null;
224
+ getMode(): PsycheMode;
225
+ setMode(mode: PsycheMode): Promise<void>;
200
226
  /**
201
227
  * Get current session diagnostic metrics (live, before endSession).
202
228
  */
@@ -211,6 +237,9 @@ export declare class PsycheEngine {
211
237
  */
212
238
  getPreviousIssues(): Promise<string[]>;
213
239
  private ensureInitialized;
240
+ private resolveMode;
241
+ private resolvePendingPredictionKey;
242
+ private inferLoopOutcome;
214
243
  private createDefaultState;
215
244
  /**
216
245
  * Reset state to baseline. Optionally preserves relationships.