@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.
- package/index.js +155 -0
- package/openclaw.plugin.json +2 -2
- package/package.json +4 -4
- package/runtime/connectors/social-community/moltbook/api-client.d.ts +27 -0
- package/runtime/connectors/social-community/moltbook/api-client.js +71 -0
- package/runtime/connectors/social-community/moltbook/index.d.ts +1 -0
- package/runtime/connectors/social-community/moltbook/index.js +1 -0
- package/runtime/core/second-nature/guidance/user-reply-continuity.d.ts +50 -0
- package/runtime/core/second-nature/guidance/user-reply-continuity.js +80 -0
- package/runtime/core/second-nature/heartbeat/heartbeat-executor.d.ts +97 -0
- package/runtime/core/second-nature/heartbeat/heartbeat-executor.js +112 -0
- package/runtime/core/second-nature/heartbeat/heartbeat-loop.d.ts +42 -0
- package/runtime/core/second-nature/heartbeat/heartbeat-loop.js +73 -0
- package/runtime/core/second-nature/heartbeat/index.d.ts +5 -0
- package/runtime/core/second-nature/heartbeat/index.js +4 -0
- package/runtime/core/second-nature/heartbeat/scope-router.d.ts +28 -0
- package/runtime/core/second-nature/heartbeat/scope-router.js +46 -0
- package/runtime/core/second-nature/heartbeat/signal.d.ts +35 -0
- package/runtime/core/second-nature/heartbeat/signal.js +8 -0
- package/runtime/core/second-nature/heartbeat/snapshot-builder.d.ts +33 -0
- package/runtime/core/second-nature/heartbeat/snapshot-builder.js +18 -0
- package/runtime/core/second-nature/runtime/service-entry.js +1 -1
- package/runtime/guidance/guidance-assembler.js +1 -1
- package/runtime/guidance/persona-selection.js +5 -0
- package/runtime/guidance/template-registry.d.ts +1 -1
- package/runtime/guidance/types.d.ts +1 -1
- package/runtime/observability/index.d.ts +1 -1
- package/runtime/observability/services/decision-ledger.d.ts +13 -0
- package/runtime/observability/services/decision-ledger.js +42 -0
- package/index.ts +0 -200
- 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
|
+
};
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "second-nature",
|
|
3
3
|
"name": "Second Nature",
|
|
4
|
-
"version": "0.1.
|
|
5
|
-
"entry": "./index.
|
|
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
|
+
"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.
|
|
17
|
+
"main": "./index.js",
|
|
18
18
|
"files": [
|
|
19
|
-
"index.
|
|
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.
|
|
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
|
+
}
|
|
@@ -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
|
+
}
|