@haaaiawd/second-nature 0.1.3 → 0.1.4

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.
Files changed (31) hide show
  1. package/index.js +155 -0
  2. package/openclaw.plugin.json +2 -2
  3. package/package.json +4 -4
  4. package/runtime/connectors/social-community/moltbook/api-client.d.ts +27 -0
  5. package/runtime/connectors/social-community/moltbook/api-client.js +71 -0
  6. package/runtime/connectors/social-community/moltbook/index.d.ts +1 -0
  7. package/runtime/connectors/social-community/moltbook/index.js +1 -0
  8. package/runtime/core/second-nature/guidance/user-reply-continuity.d.ts +50 -0
  9. package/runtime/core/second-nature/guidance/user-reply-continuity.js +80 -0
  10. package/runtime/core/second-nature/heartbeat/heartbeat-executor.d.ts +97 -0
  11. package/runtime/core/second-nature/heartbeat/heartbeat-executor.js +112 -0
  12. package/runtime/core/second-nature/heartbeat/heartbeat-loop.d.ts +42 -0
  13. package/runtime/core/second-nature/heartbeat/heartbeat-loop.js +73 -0
  14. package/runtime/core/second-nature/heartbeat/index.d.ts +5 -0
  15. package/runtime/core/second-nature/heartbeat/index.js +4 -0
  16. package/runtime/core/second-nature/heartbeat/scope-router.d.ts +28 -0
  17. package/runtime/core/second-nature/heartbeat/scope-router.js +46 -0
  18. package/runtime/core/second-nature/heartbeat/signal.d.ts +35 -0
  19. package/runtime/core/second-nature/heartbeat/signal.js +8 -0
  20. package/runtime/core/second-nature/heartbeat/snapshot-builder.d.ts +33 -0
  21. package/runtime/core/second-nature/heartbeat/snapshot-builder.js +18 -0
  22. package/runtime/core/second-nature/runtime/service-entry.js +1 -1
  23. package/runtime/guidance/guidance-assembler.js +1 -1
  24. package/runtime/guidance/persona-selection.js +5 -0
  25. package/runtime/guidance/template-registry.d.ts +1 -1
  26. package/runtime/guidance/types.d.ts +1 -1
  27. package/runtime/observability/index.d.ts +1 -1
  28. package/runtime/observability/services/decision-ledger.d.ts +13 -0
  29. package/runtime/observability/services/decision-ledger.js +42 -0
  30. package/index.ts +0 -200
  31. package/runtime/setup/HOST_SETUP.md +0 -112
package/index.js ADDED
@@ -0,0 +1,155 @@
1
+ import { createRequire } from "node:module";
2
+ function createFallbackCommands() {
3
+ const commandNames = ["status", "policy", "credential", "quiet", "report", "session", "audit", "explain"];
4
+ return commandNames.map((name) => ({
5
+ name,
6
+ description: `Fallback command shell for ${name}`,
7
+ execute: async (_input) => ({
8
+ ok: false,
9
+ command: name,
10
+ message: "Plugin loaded in packaging fallback mode; reinstall full workspace build for command runtime.",
11
+ }),
12
+ }));
13
+ }
14
+ function resolveCommandRouterSafe() {
15
+ const require = createRequire(import.meta.url);
16
+ try {
17
+ const mod = require("./runtime/cli/index.js");
18
+ if (mod?.createCommandRouter) {
19
+ return mod.createCommandRouter();
20
+ }
21
+ }
22
+ catch {
23
+ // fall through to fallback router
24
+ }
25
+ const commands = createFallbackCommands();
26
+ return {
27
+ commands,
28
+ resolve(name) {
29
+ return commands.find((command) => command.name === name);
30
+ },
31
+ };
32
+ }
33
+ function createRuntimeService() {
34
+ const require = createRequire(import.meta.url);
35
+ try {
36
+ const runtimeMod = require("./runtime/core/second-nature/runtime/service-entry.js");
37
+ if (runtimeMod?.startRuntimeService) {
38
+ const handle = runtimeMod.startRuntimeService();
39
+ return {
40
+ id: "second-nature-runtime",
41
+ start() {
42
+ return { ready: handle.ready, version: handle.version };
43
+ },
44
+ };
45
+ }
46
+ }
47
+ catch {
48
+ // fall through to minimal service shell
49
+ }
50
+ return {
51
+ id: "second-nature-runtime",
52
+ start() {
53
+ return { ready: true, version: "0.1.4-minimal" };
54
+ },
55
+ };
56
+ }
57
+ function createLifecycleService() {
58
+ const require = createRequire(import.meta.url);
59
+ try {
60
+ const lifecycleMod = require("./runtime/core/second-nature/runtime/lifecycle-service.js");
61
+ if (lifecycleMod?.recordRegistration) {
62
+ return {
63
+ id: "second-nature-lifecycle",
64
+ start() {
65
+ const state = lifecycleMod.recordRegistration();
66
+ return { phase: state.phase, registerCount: state.registerCount };
67
+ },
68
+ };
69
+ }
70
+ }
71
+ catch {
72
+ // fall through to minimal lifecycle shell
73
+ }
74
+ let registerCount = 0;
75
+ return {
76
+ id: "second-nature-lifecycle",
77
+ start() {
78
+ registerCount += 1;
79
+ return { phase: registerCount === 1 ? "loading" : "reloading", registerCount };
80
+ },
81
+ };
82
+ }
83
+ export default {
84
+ id: "second-nature",
85
+ name: "Second Nature",
86
+ description: "Registers command/tool/service surface with load-reload lifecycle semantics.",
87
+ register(api) {
88
+ const router = resolveCommandRouterSafe();
89
+ const runtimeService = createRuntimeService();
90
+ const lifecycleService = createLifecycleService();
91
+ api.registerService(runtimeService);
92
+ api.registerService(lifecycleService);
93
+ api.registerCli(({ program }) => {
94
+ void program;
95
+ }, { commands: ["second-nature"] });
96
+ api.registerCommand({
97
+ name: "second-nature",
98
+ description: "Route Agent-facing operational commands for Second Nature.",
99
+ acceptsArgs: true,
100
+ handler: async (ctx) => {
101
+ const command = ctx.args?.trim();
102
+ if (!command) {
103
+ return {
104
+ text: JSON.stringify({ ok: false, message: "Missing command argument." }),
105
+ };
106
+ }
107
+ const resolved = router.resolve(command);
108
+ if (!resolved) {
109
+ return {
110
+ text: JSON.stringify({ ok: false, command, message: "Unknown Second Nature command." }),
111
+ };
112
+ }
113
+ const result = await resolved.execute();
114
+ return {
115
+ text: JSON.stringify(result),
116
+ };
117
+ },
118
+ });
119
+ api.registerTool({
120
+ name: "second_nature_ops",
121
+ description: "Access the Second Nature command surface through a single tool shell.",
122
+ parameters: {
123
+ type: "object",
124
+ additionalProperties: false,
125
+ properties: {
126
+ command: { type: "string" },
127
+ args: { type: "object", additionalProperties: true }
128
+ },
129
+ required: ["command"]
130
+ },
131
+ async execute(_id, params) {
132
+ const resolved = router.resolve(params.command);
133
+ if (!resolved) {
134
+ return {
135
+ content: [
136
+ {
137
+ type: "text",
138
+ text: JSON.stringify({ ok: false, message: "Unknown Second Nature command." }),
139
+ },
140
+ ],
141
+ };
142
+ }
143
+ const result = await resolved.execute(params.args);
144
+ return {
145
+ content: [
146
+ {
147
+ type: "text",
148
+ text: JSON.stringify(result),
149
+ },
150
+ ],
151
+ };
152
+ },
153
+ });
154
+ },
155
+ };
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "id": "second-nature",
3
3
  "name": "Second Nature",
4
- "version": "0.1.3",
5
- "entry": "./index.ts",
4
+ "version": "0.1.4",
5
+ "entry": "./index.js",
6
6
  "description": "OpenClaw native plugin package for continuity, explain, and recovery operations.",
7
7
  "capabilities": {
8
8
  "commands": ["second-nature"],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@haaaiawd/second-nature",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "OpenClaw native plugin for long-running agent continuity, Quiet memory curation, and explainable operator flows.",
5
5
  "keywords": [
6
6
  "openclaw",
@@ -14,9 +14,9 @@
14
14
  ],
15
15
  "license": "Apache-2.0",
16
16
  "type": "module",
17
- "main": "./index.ts",
17
+ "main": "./index.js",
18
18
  "files": [
19
- "index.ts",
19
+ "index.js",
20
20
  "openclaw.plugin.json",
21
21
  "runtime/"
22
22
  ],
@@ -26,7 +26,7 @@
26
26
  "openclaw": {
27
27
  "manifest": "./openclaw.plugin.json",
28
28
  "extensions": [
29
- "./index.ts"
29
+ "./index.js"
30
30
  ]
31
31
  },
32
32
  "dependencies": {
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Moltbook REST API Client
3
+ *
4
+ * Implements the MoltbookApiClient interface for direct REST API access.
5
+ * Uses fetch() with Bearer token authentication.
6
+ *
7
+ * API Reference: moltbook.apidog.io
8
+ * Auth: OAuth 2.0 via CLI (moltbook login)
9
+ *
10
+ * Per T3.1.1: This provides a minimal real client for feed.read capability,
11
+ * with a replaceable seam for skill/CLI fallback.
12
+ */
13
+ import type { MoltbookApiClient } from "./adapter.js";
14
+ export interface MoltbookApiConfig {
15
+ /** Base URL for Moltbook REST API */
16
+ baseUrl: string;
17
+ /** OAuth Bearer token */
18
+ accessToken: string;
19
+ /** Request timeout in milliseconds */
20
+ timeoutMs?: number;
21
+ }
22
+ export declare function createMoltbookApiClient(config: MoltbookApiConfig): MoltbookApiClient;
23
+ export declare class MoltbookApiError extends Error {
24
+ readonly statusCode: number;
25
+ readonly responseBody?: string | undefined;
26
+ constructor(statusCode: number, message: string, responseBody?: string | undefined);
27
+ }
@@ -0,0 +1,71 @@
1
+ export function createMoltbookApiClient(config) {
2
+ const { baseUrl, accessToken, timeoutMs = 5000 } = config;
3
+ async function request(path, options = {}) {
4
+ const controller = new AbortController();
5
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
6
+ try {
7
+ const response = await fetch(`${baseUrl}${path}`, {
8
+ ...options,
9
+ headers: {
10
+ "Authorization": `Bearer ${accessToken}`,
11
+ "Content-Type": "application/json",
12
+ ...options.headers,
13
+ },
14
+ signal: controller.signal,
15
+ });
16
+ if (!response.ok) {
17
+ const errorBody = await response.text().catch(() => "");
18
+ throw new MoltbookApiError(response.status, `Moltbook API error: ${response.status} ${response.statusText}`, errorBody);
19
+ }
20
+ return response.json();
21
+ }
22
+ finally {
23
+ clearTimeout(timeoutId);
24
+ }
25
+ }
26
+ return {
27
+ async readFeed(payload) {
28
+ const params = new URLSearchParams();
29
+ if (payload.limit)
30
+ params.set("limit", String(payload.limit));
31
+ if (payload.offset)
32
+ params.set("offset", String(payload.offset));
33
+ if (payload.sort)
34
+ params.set("sort", String(payload.sort));
35
+ const queryString = params.toString();
36
+ return request(`/api/v1/feed${queryString ? `?${queryString}` : ""}`);
37
+ },
38
+ async publishPost(payload) {
39
+ return request("/api/v1/posts", {
40
+ method: "POST",
41
+ body: JSON.stringify({
42
+ content: payload.content,
43
+ link: payload.link,
44
+ community: payload.community,
45
+ }),
46
+ });
47
+ },
48
+ async replyComment(payload) {
49
+ const postId = payload.postId;
50
+ if (!postId) {
51
+ throw new MoltbookApiError(400, "postId is required for comment.reply");
52
+ }
53
+ return request(`/api/v1/posts/${postId}/comments`, {
54
+ method: "POST",
55
+ body: JSON.stringify({
56
+ content: payload.content,
57
+ }),
58
+ });
59
+ },
60
+ };
61
+ }
62
+ export class MoltbookApiError extends Error {
63
+ statusCode;
64
+ responseBody;
65
+ constructor(statusCode, message, responseBody) {
66
+ super(message);
67
+ this.statusCode = statusCode;
68
+ this.responseBody = responseBody;
69
+ this.name = "MoltbookApiError";
70
+ }
71
+ }
@@ -1,2 +1,3 @@
1
1
  export * from "./manifest.js";
2
2
  export * from "./adapter.js";
3
+ export * from "./api-client.js";
@@ -1,2 +1,3 @@
1
1
  export * from "./manifest.js";
2
2
  export * from "./adapter.js";
3
+ export * from "./api-client.js";
@@ -0,0 +1,50 @@
1
+ /**
2
+ * User Reply Light Continuity Contract
3
+ *
4
+ * Per T6.1.1: Provides very light continuity guidance for direct user replies.
5
+ * This is separate from the platform `reply` scene - it only provides
6
+ * lightweight persona continuity and tone consistency for user-facing chat.
7
+ *
8
+ * Key differences from platform `reply` scene:
9
+ * - No platform-specific impulses
10
+ * - No comment/reply formatting constraints
11
+ * - Only persona continuity and minimal tone guidance
12
+ * - Does not enter the reply scene impulse system
13
+ */
14
+ import type { GuidanceFallback, GuidancePayload } from "../../../guidance/index.js";
15
+ import type { PersonaCandidate } from "../../../guidance/index.js";
16
+ /**
17
+ * Scene context for user reply - uses a distinct scene type
18
+ * to avoid confusion with platform reply scene.
19
+ */
20
+ export declare const USER_REPLY_SCENE_TYPE: "user_reply";
21
+ export type UserReplySceneType = typeof USER_REPLY_SCENE_TYPE;
22
+ /**
23
+ * Build very light continuity guidance for direct user replies.
24
+ *
25
+ * Returns a minimal guidance payload with:
26
+ * - Light atmosphere (continuity-focused)
27
+ * - NO impulses (unlike platform reply scene)
28
+ * - Optional persona reinforcement (1-2 snippets max)
29
+ * - Minimal output guard (tone consistency only)
30
+ */
31
+ export declare function buildLightReplyContinuity(input: {
32
+ replyContext: {
33
+ recentTone?: string;
34
+ lastInteractionSummary?: string;
35
+ };
36
+ personaCandidates?: PersonaCandidate[];
37
+ }): Promise<GuidancePayload | GuidanceFallback>;
38
+ /**
39
+ * Check if an input should be classified as direct user reply.
40
+ *
41
+ * Classification criteria:
42
+ * - Trigger source is user_reply
43
+ * - Not a platform comment/reply
44
+ * - Not an explicit task delegation
45
+ */
46
+ export declare function isDirectUserReply(input: {
47
+ triggerSource: string;
48
+ isPlatformReply: boolean;
49
+ isExplicitTask: boolean;
50
+ }): boolean;
@@ -0,0 +1,80 @@
1
+ import { buildMinimalGuidanceFallback } from "../../../guidance/fallback.js";
2
+ import { selectPersonaSnippets } from "../../../guidance/persona-selection.js";
3
+ import { getBaselineAtmosphereTemplate } from "../../../guidance/template-registry.js";
4
+ /**
5
+ * Scene context for user reply - uses a distinct scene type
6
+ * to avoid confusion with platform reply scene.
7
+ */
8
+ export const USER_REPLY_SCENE_TYPE = "user_reply";
9
+ /**
10
+ * Build very light continuity guidance for direct user replies.
11
+ *
12
+ * Returns a minimal guidance payload with:
13
+ * - Light atmosphere (continuity-focused)
14
+ * - NO impulses (unlike platform reply scene)
15
+ * - Optional persona reinforcement (1-2 snippets max)
16
+ * - Minimal output guard (tone consistency only)
17
+ */
18
+ export async function buildLightReplyContinuity(input) {
19
+ const sceneContext = {
20
+ sceneType: "user_reply",
21
+ mode: "active",
22
+ riskLevel: "low",
23
+ sceneSummary: "direct user reply continuity",
24
+ };
25
+ try {
26
+ // Light atmosphere - continuity focused
27
+ const atmosphereTemplate = getBaselineAtmosphereTemplate();
28
+ const atmosphere = {
29
+ kind: "atmosphere",
30
+ text: `保持同一个人的语气。${input.replyContext.recentTone ? `最近语气参考:${input.replyContext.recentTone}` : "延续既有连续感。"}`,
31
+ openness: "open",
32
+ pressureLabels: ["user_reply", "continuity"],
33
+ reviewStatus: atmosphereTemplate.reviewStatus,
34
+ };
35
+ // NO impulses for user reply - this is the key difference from platform reply
36
+ const impulses = [];
37
+ // Minimal persona reinforcement - only if candidates available
38
+ let personaReinforcement = [];
39
+ if (input.personaCandidates && input.personaCandidates.length > 0) {
40
+ const personaDecision = selectPersonaSnippets({
41
+ sceneContext,
42
+ candidates: input.personaCandidates.slice(0, 2), // Max 2 snippets for light continuity
43
+ });
44
+ personaReinforcement = personaDecision.snippets;
45
+ }
46
+ // Minimal output guard - tone consistency only
47
+ const outputGuard = {
48
+ kind: "output_guard",
49
+ constraints: [
50
+ "保持对话语气,不要用帖子回复腔",
51
+ "延续同一个人格连续性",
52
+ ],
53
+ hardGuardPriority: true,
54
+ };
55
+ return {
56
+ scene: sceneContext,
57
+ atmosphere,
58
+ impulses,
59
+ personaReinforcement,
60
+ outputGuard,
61
+ };
62
+ }
63
+ catch {
64
+ // Fallback to minimal guidance
65
+ return buildMinimalGuidanceFallback(sceneContext);
66
+ }
67
+ }
68
+ /**
69
+ * Check if an input should be classified as direct user reply.
70
+ *
71
+ * Classification criteria:
72
+ * - Trigger source is user_reply
73
+ * - Not a platform comment/reply
74
+ * - Not an explicit task delegation
75
+ */
76
+ export function isDirectUserReply(input) {
77
+ return (input.triggerSource === "user_reply" &&
78
+ !input.isPlatformReply &&
79
+ !input.isExplicitTask);
80
+ }
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Heartbeat Executor Bridge
3
+ *
4
+ * Connects the decision loop to guidance and effect dispatch.
5
+ * Per T2.2.2:
6
+ * - Guidance is only requested when an allow verdict enters a generative scene
7
+ * - External effects only occur under allow verdict
8
+ * - Guidance payload participates in generative context assembly within control-plane,
9
+ * and does NOT cross into connector execution boundary
10
+ *
11
+ * This module does NOT make guidance a decision maker:
12
+ * - Guidance does not decide allow/deny
13
+ * - Guidance does not replace guards
14
+ * - Guidance does not directly drive connector executor
15
+ */
16
+ import type { GuidanceMode } from "../../../guidance/index.js";
17
+ import type { GuardVerdict } from "../types.js";
18
+ import { requestGuidance, type RequestGuidanceResult } from "../guidance/request-guidance.js";
19
+ import { applyGuidance, type AppliedGuidanceContext } from "../guidance/apply-guidance.js";
20
+ import { type AllowedIntent, type DispatchResult } from "../orchestrator/effect-dispatcher.js";
21
+ import type { LeaseManager } from "../orchestrator/lease-manager.js";
22
+ import type { IntentCommitPort, ConnectorExecutor, CheckpointPort, MemoryPort, ReflectionPort } from "../orchestrator/effect-dispatcher.js";
23
+ export interface GuidanceBridgeDeps {
24
+ /** Request guidance from the behavioral guidance system */
25
+ requestGuidance: typeof requestGuidance;
26
+ /** Apply guidance payload into context */
27
+ applyGuidance: typeof applyGuidance;
28
+ }
29
+ export interface EffectDispatchDeps {
30
+ leaseManager: LeaseManager;
31
+ commitPort: IntentCommitPort;
32
+ connectorExecutor: ConnectorExecutor;
33
+ checkpointPort: CheckpointPort;
34
+ memoryPort: MemoryPort;
35
+ reflectionPort: ReflectionPort;
36
+ }
37
+ export interface HeartbeatExecutorDeps {
38
+ guidance: GuidanceBridgeDeps;
39
+ effects: EffectDispatchDeps;
40
+ }
41
+ /**
42
+ * Result of the guidance bridge step.
43
+ * Guidance is only requested for generative scenes under allow verdict.
44
+ */
45
+ export interface GuidanceBridgeResult {
46
+ intent: AllowedIntent;
47
+ guidanceResult?: RequestGuidanceResult;
48
+ appliedContext?: AppliedGuidanceContext;
49
+ }
50
+ /**
51
+ * Result of the full heartbeat execution cycle.
52
+ */
53
+ export interface HeartbeatExecutionResult {
54
+ decisionId: string;
55
+ intentId: string;
56
+ guardVerdict: GuardVerdict;
57
+ guidance?: GuidanceBridgeResult;
58
+ dispatch?: DispatchResult;
59
+ }
60
+ /**
61
+ * Request guidance for a selected intent.
62
+ *
63
+ * Guidance is only requested when:
64
+ * - The intent kind maps to a generative scene (social, outreach, explain)
65
+ * - Maintenance, reflection, and work do not request guidance
66
+ *
67
+ * The guidance payload is used for context assembly within control-plane.
68
+ * It does NOT cross the connector execution boundary.
69
+ */
70
+ export declare function requestGuidanceForIntent(intent: AllowedIntent, mode: GuidanceMode, deps: GuidanceBridgeDeps): Promise<GuidanceBridgeResult>;
71
+ /**
72
+ * Dispatch effects for an allowed intent.
73
+ *
74
+ * This function enforces the allow-only boundary:
75
+ * - Only called when guard verdict is "allow"
76
+ * - Creates a decision context and dispatches through EffectDispatcher
77
+ * - Guidance context stays within control-plane and is NOT passed to connector executor
78
+ *
79
+ * The connector executor receives the original intent payload without any
80
+ * guidance-derived fields. Guidance participates in control-plane context
81
+ * assembly but does not leak into the connector execution boundary.
82
+ */
83
+ export declare function dispatchAllowedEffect(intent: AllowedIntent, deps: EffectDispatchDeps): Promise<DispatchResult>;
84
+ /**
85
+ * Full heartbeat execution: guidance bridge + allow-only effect dispatch.
86
+ *
87
+ * Flow:
88
+ * 1. Check guard verdict — non-allow paths skip guidance entirely
89
+ * 2. For allow verdict: request guidance (if generative scene), then dispatch effect
90
+ * 3. Return execution result with guidance and dispatch info
91
+ *
92
+ * Per T2.2.2 boundaries:
93
+ * - Guidance is NOT requested for deny/defer verdicts
94
+ * - Guidance payload does NOT cross into connector execution boundary
95
+ * - External effects only occur under allow verdict
96
+ */
97
+ export declare function executeHeartbeatCycle(intent: AllowedIntent, guardVerdict: GuardVerdict, mode: GuidanceMode, deps: HeartbeatExecutorDeps): Promise<HeartbeatExecutionResult>;
@@ -0,0 +1,112 @@
1
+ import { EffectDispatcher, buildDecisionContext } from "../orchestrator/effect-dispatcher.js";
2
+ /**
3
+ * Map an intent kind to its guidance scene type.
4
+ * Only generative scenes (social, outreach, explain) request guidance.
5
+ * Maintenance, reflection, and work do not request guidance.
6
+ */
7
+ function intentKindToScene(kind) {
8
+ switch (kind) {
9
+ case "social":
10
+ return "social";
11
+ case "outreach":
12
+ return "outreach";
13
+ case "exploration":
14
+ return "explain";
15
+ default:
16
+ return null;
17
+ }
18
+ }
19
+ /**
20
+ * Build a scene context from an allowed intent and runtime mode.
21
+ *
22
+ * Mode comes from the actual runtime context (active/quiet/maintenance_only/paused_for_interrupt),
23
+ * not hardcoded.
24
+ */
25
+ function buildSceneContext(intent, mode) {
26
+ return {
27
+ sceneType: intentKindToScene(intent.kind) ?? "explain",
28
+ mode,
29
+ sceneSummary: intent.summary,
30
+ };
31
+ }
32
+ /**
33
+ * Request guidance for a selected intent.
34
+ *
35
+ * Guidance is only requested when:
36
+ * - The intent kind maps to a generative scene (social, outreach, explain)
37
+ * - Maintenance, reflection, and work do not request guidance
38
+ *
39
+ * The guidance payload is used for context assembly within control-plane.
40
+ * It does NOT cross the connector execution boundary.
41
+ */
42
+ export async function requestGuidanceForIntent(intent, mode, deps) {
43
+ const sceneType = intentKindToScene(intent.kind);
44
+ if (!sceneType) {
45
+ // Non-generative intents don't request guidance
46
+ return { intent };
47
+ }
48
+ const sceneContext = buildSceneContext(intent, mode);
49
+ const guidanceResult = await deps.requestGuidance({ sceneContext });
50
+ const appliedContext = deps.applyGuidance(guidanceResult.guidance);
51
+ return {
52
+ intent,
53
+ guidanceResult,
54
+ appliedContext,
55
+ };
56
+ }
57
+ /**
58
+ * Dispatch effects for an allowed intent.
59
+ *
60
+ * This function enforces the allow-only boundary:
61
+ * - Only called when guard verdict is "allow"
62
+ * - Creates a decision context and dispatches through EffectDispatcher
63
+ * - Guidance context stays within control-plane and is NOT passed to connector executor
64
+ *
65
+ * The connector executor receives the original intent payload without any
66
+ * guidance-derived fields. Guidance participates in control-plane context
67
+ * assembly but does not leak into the connector execution boundary.
68
+ */
69
+ export async function dispatchAllowedEffect(intent, deps) {
70
+ const dispatcher = new EffectDispatcher(deps.leaseManager, deps.commitPort, deps.connectorExecutor, deps.checkpointPort, deps.memoryPort, deps.reflectionPort);
71
+ const decisionContext = buildDecisionContext({
72
+ tickId: `tick:${Date.now()}`,
73
+ intentId: intent.id,
74
+ });
75
+ // Dispatch with the original intent payload.
76
+ // Guidance context (if any) remains within control-plane boundary
77
+ // and is NOT embedded in the connector payload.
78
+ return dispatcher.dispatchEffect(intent, decisionContext);
79
+ }
80
+ /**
81
+ * Full heartbeat execution: guidance bridge + allow-only effect dispatch.
82
+ *
83
+ * Flow:
84
+ * 1. Check guard verdict — non-allow paths skip guidance entirely
85
+ * 2. For allow verdict: request guidance (if generative scene), then dispatch effect
86
+ * 3. Return execution result with guidance and dispatch info
87
+ *
88
+ * Per T2.2.2 boundaries:
89
+ * - Guidance is NOT requested for deny/defer verdicts
90
+ * - Guidance payload does NOT cross into connector execution boundary
91
+ * - External effects only occur under allow verdict
92
+ */
93
+ export async function executeHeartbeatCycle(intent, guardVerdict, mode, deps) {
94
+ // Non-allow verdicts: skip guidance entirely, no effect dispatch
95
+ if (guardVerdict !== "allow") {
96
+ return {
97
+ decisionId: `decision:${intent.id}:${Date.now()}`,
98
+ intentId: intent.id,
99
+ guardVerdict,
100
+ };
101
+ }
102
+ // Allow verdict: request guidance for generative scenes, then dispatch
103
+ const guidance = await requestGuidanceForIntent(intent, mode, deps.guidance);
104
+ const dispatch = await dispatchAllowedEffect(intent, deps.effects);
105
+ return {
106
+ decisionId: `decision:${intent.id}:${Date.now()}`,
107
+ intentId: intent.id,
108
+ guardVerdict,
109
+ guidance,
110
+ dispatch,
111
+ };
112
+ }