pi-xmpp 0.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/AGENTS.md +3 -0
- package/BACKLOG.md +17 -0
- package/CHANGELOG.md +15 -0
- package/LICENSE +9 -0
- package/README.md +124 -0
- package/api/commands.ts +11 -0
- package/api/inbound.ts +14 -0
- package/api/outbound.ts +11 -0
- package/api/status.ts +12 -0
- package/api/updates.ts +41 -0
- package/index.ts +325 -0
- package/lib/commands.ts +245 -0
- package/lib/config.ts +221 -0
- package/lib/inbound.ts +143 -0
- package/lib/lifecycle.ts +105 -0
- package/lib/model.ts +96 -0
- package/lib/outbound.ts +138 -0
- package/lib/pi.ts +165 -0
- package/lib/prompts.ts +95 -0
- package/lib/queue.ts +129 -0
- package/lib/routing.ts +196 -0
- package/lib/runtime.ts +148 -0
- package/lib/status.ts +144 -0
- package/lib/xmpp-api.ts +352 -0
- package/lib/xmpp.d.ts +59 -0
- package/package.json +71 -0
package/lib/model.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XMPP model and thinking level helpers
|
|
3
|
+
* Zones: pi agent model control, xmpp controls
|
|
4
|
+
* Owns model identity, thinking levels, and current-model state
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { ExtensionContext } from "./pi.ts";
|
|
8
|
+
|
|
9
|
+
export interface MenuModel {
|
|
10
|
+
provider: string;
|
|
11
|
+
id: string;
|
|
12
|
+
name?: string;
|
|
13
|
+
reasoning?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type ThinkingLevel =
|
|
17
|
+
| "off"
|
|
18
|
+
| "minimal"
|
|
19
|
+
| "low"
|
|
20
|
+
| "medium"
|
|
21
|
+
| "high"
|
|
22
|
+
| "xhigh";
|
|
23
|
+
|
|
24
|
+
export interface ScopedXmppModel<TModel extends MenuModel = MenuModel> {
|
|
25
|
+
model: TModel;
|
|
26
|
+
thinkingLevel?: ThinkingLevel;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const THINKING_LEVELS: readonly ThinkingLevel[] = [
|
|
30
|
+
"off",
|
|
31
|
+
"minimal",
|
|
32
|
+
"low",
|
|
33
|
+
"medium",
|
|
34
|
+
"high",
|
|
35
|
+
"xhigh",
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
export interface CurrentModelStore<
|
|
39
|
+
TContext,
|
|
40
|
+
TModel extends MenuModel = MenuModel,
|
|
41
|
+
> {
|
|
42
|
+
get: (ctx: TContext) => TModel | undefined;
|
|
43
|
+
getStored: () => TModel | undefined;
|
|
44
|
+
set: (model: TModel | undefined) => void;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface CurrentModelUpdateRuntime<
|
|
48
|
+
TContext,
|
|
49
|
+
TModel extends MenuModel = MenuModel,
|
|
50
|
+
> {
|
|
51
|
+
setCurrentModel: (model: TModel | undefined, ctx: TContext) => void;
|
|
52
|
+
onModelSelect: (event: { model: TModel | undefined }, ctx: TContext) => void;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export type CurrentModelRuntime<
|
|
56
|
+
TContext,
|
|
57
|
+
TModel extends MenuModel = MenuModel,
|
|
58
|
+
> = CurrentModelStore<TContext, TModel> &
|
|
59
|
+
CurrentModelUpdateRuntime<TContext, TModel>;
|
|
60
|
+
|
|
61
|
+
export function createCurrentModelStore<
|
|
62
|
+
TContext,
|
|
63
|
+
TModel extends MenuModel = MenuModel,
|
|
64
|
+
>(
|
|
65
|
+
getContextModel: (ctx: TContext) => TModel | undefined,
|
|
66
|
+
): CurrentModelStore<TContext, TModel> {
|
|
67
|
+
let currentModel: TModel | undefined;
|
|
68
|
+
return {
|
|
69
|
+
get: (ctx) => currentModel ?? getContextModel(ctx),
|
|
70
|
+
getStored: () => currentModel,
|
|
71
|
+
set: (model) => {
|
|
72
|
+
currentModel = model;
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function createCurrentModelUpdateRuntime<
|
|
78
|
+
TContext extends ExtensionContext,
|
|
79
|
+
TModel extends MenuModel = MenuModel,
|
|
80
|
+
>(
|
|
81
|
+
deps: CurrentModelStore<TContext, TModel> & {
|
|
82
|
+
setModel: (model: TModel | undefined) => void;
|
|
83
|
+
},
|
|
84
|
+
): CurrentModelUpdateRuntime<TContext, TModel> {
|
|
85
|
+
return {
|
|
86
|
+
setCurrentModel(model, ctx) {
|
|
87
|
+
deps.set(model);
|
|
88
|
+
if (model) {
|
|
89
|
+
deps.setModel(model as Parameters<typeof deps.setModel>[0]);
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
onModelSelect(event, ctx) {
|
|
93
|
+
deps.set(event.model);
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|
package/lib/outbound.ts
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XMPP outbound surface helpers
|
|
3
|
+
* Zones: xmpp outbound, message delivery
|
|
4
|
+
* Owns configured outbound handler execution, text transforms, and direct message sending
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { XmppClientInstance } from "./xmpp-api.ts";
|
|
8
|
+
import { getBareJid } from "./routing.ts";
|
|
9
|
+
|
|
10
|
+
const OUTBOUND_HANDLER_REGISTRY_KEY = "__piXmppOutboundHandlers__";
|
|
11
|
+
|
|
12
|
+
export interface XmppOutboundProgrammaticHandlerInput {
|
|
13
|
+
body: string;
|
|
14
|
+
to: string;
|
|
15
|
+
toBare: string;
|
|
16
|
+
type: string;
|
|
17
|
+
isGroup: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface XmppOutboundProgrammaticHandlerResult {
|
|
21
|
+
handled: boolean;
|
|
22
|
+
body?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface XmppOutboundProgrammaticHandler {
|
|
26
|
+
(
|
|
27
|
+
input: XmppOutboundProgrammaticHandlerInput,
|
|
28
|
+
):
|
|
29
|
+
| XmppOutboundProgrammaticHandlerResult
|
|
30
|
+
| Promise<XmppOutboundProgrammaticHandlerResult>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Register a programmatic outbound handler.
|
|
35
|
+
* Handlers can transform or intercept outgoing messages.
|
|
36
|
+
*/
|
|
37
|
+
export function registerXmppOutboundHandler(
|
|
38
|
+
handler: XmppOutboundProgrammaticHandler,
|
|
39
|
+
): void {
|
|
40
|
+
const registry = getOutboundHandlerRegistry();
|
|
41
|
+
registry.push(handler);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function getOutboundHandlerRegistry(): XmppOutboundProgrammaticHandler[] {
|
|
45
|
+
const globals = globalThis as Record<string, unknown>;
|
|
46
|
+
if (!globals[OUTBOUND_HANDLER_REGISTRY_KEY]) {
|
|
47
|
+
globals[OUTBOUND_HANDLER_REGISTRY_KEY] = [];
|
|
48
|
+
}
|
|
49
|
+
return globals[OUTBOUND_HANDLER_REGISTRY_KEY] as XmppOutboundProgrammaticHandler[];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function getXmppOutboundHandlers(): XmppOutboundProgrammaticHandler[] {
|
|
53
|
+
return getOutboundHandlerRegistry();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface XmppSendMessageOptions {
|
|
57
|
+
type?: string;
|
|
58
|
+
subject?: string;
|
|
59
|
+
thread?: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Send a message via the XMPP client.
|
|
64
|
+
* Runs through the outbound handler pipeline before sending.
|
|
65
|
+
*/
|
|
66
|
+
export async function sendXmppMessage(
|
|
67
|
+
client: XmppClientInstance,
|
|
68
|
+
to: string,
|
|
69
|
+
body: string,
|
|
70
|
+
options?: XmppSendMessageOptions,
|
|
71
|
+
): Promise<void> {
|
|
72
|
+
const toBare = getBareJid(to);
|
|
73
|
+
const isGroup = options?.type === "groupchat";
|
|
74
|
+
let finalBody = body;
|
|
75
|
+
let handled = false;
|
|
76
|
+
|
|
77
|
+
// Run through outbound handler pipeline
|
|
78
|
+
const handlers = getXmppOutboundHandlers();
|
|
79
|
+
for (const handler of handlers) {
|
|
80
|
+
try {
|
|
81
|
+
const result = await handler({
|
|
82
|
+
body: finalBody,
|
|
83
|
+
to,
|
|
84
|
+
toBare,
|
|
85
|
+
type: options?.type ?? "chat",
|
|
86
|
+
isGroup,
|
|
87
|
+
});
|
|
88
|
+
if (result.handled) {
|
|
89
|
+
handled = true;
|
|
90
|
+
if (result.body !== undefined) {
|
|
91
|
+
finalBody = result.body;
|
|
92
|
+
}
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
} catch {
|
|
96
|
+
// Silently continue
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!handled) {
|
|
101
|
+
await client.sendMessage(to, finalBody, {
|
|
102
|
+
type: options?.type,
|
|
103
|
+
subject: options?.subject,
|
|
104
|
+
thread: options?.thread,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Record a runtime event for diagnostics.
|
|
111
|
+
* Companion extensions can call this to surface diagnostics.
|
|
112
|
+
*/
|
|
113
|
+
export type XmppRuntimeEventRecorder = (
|
|
114
|
+
category: string,
|
|
115
|
+
error: unknown,
|
|
116
|
+
details?: Record<string, unknown>,
|
|
117
|
+
) => void;
|
|
118
|
+
|
|
119
|
+
const EVENT_RECORDER_KEY = "__piXmppEventRecorder__";
|
|
120
|
+
|
|
121
|
+
export function bindXmppRuntimeEventRecorder(
|
|
122
|
+
recorder: XmppRuntimeEventRecorder,
|
|
123
|
+
): void {
|
|
124
|
+
(globalThis as Record<string, unknown>)[EVENT_RECORDER_KEY] = recorder;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function recordXmppRuntimeEvent(
|
|
128
|
+
category: string,
|
|
129
|
+
error: unknown,
|
|
130
|
+
details?: Record<string, unknown>,
|
|
131
|
+
): void {
|
|
132
|
+
const recorder = (globalThis as Record<string, unknown>)[
|
|
133
|
+
EVENT_RECORDER_KEY
|
|
134
|
+
] as XmppRuntimeEventRecorder | undefined;
|
|
135
|
+
if (typeof recorder === "function") {
|
|
136
|
+
recorder(category, error, details);
|
|
137
|
+
}
|
|
138
|
+
}
|
package/lib/pi.ts
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pi SDK adapter boundary
|
|
3
|
+
* Zones: pi agent sdk boundary, shared adapters
|
|
4
|
+
* Owns direct pi SDK imports and exposes narrow bridge-facing helpers/types for the extension composition layer
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
type AgentEndEvent,
|
|
9
|
+
type AgentStartEvent,
|
|
10
|
+
type BeforeAgentStartEvent,
|
|
11
|
+
type ExtensionAPI,
|
|
12
|
+
type ExtensionCommandContext,
|
|
13
|
+
type ExtensionContext,
|
|
14
|
+
type SessionBeforeCompactEvent,
|
|
15
|
+
type SessionCompactEvent,
|
|
16
|
+
type SessionShutdownEvent,
|
|
17
|
+
type SessionStartEvent,
|
|
18
|
+
type SlashCommandInfo,
|
|
19
|
+
SettingsManager,
|
|
20
|
+
} from "@earendil-works/pi-coding-agent";
|
|
21
|
+
|
|
22
|
+
export type {
|
|
23
|
+
AgentEndEvent,
|
|
24
|
+
AgentStartEvent,
|
|
25
|
+
BeforeAgentStartEvent,
|
|
26
|
+
ExtensionAPI,
|
|
27
|
+
ExtensionCommandContext,
|
|
28
|
+
ExtensionContext,
|
|
29
|
+
SessionBeforeCompactEvent,
|
|
30
|
+
SessionCompactEvent,
|
|
31
|
+
SessionShutdownEvent,
|
|
32
|
+
SessionStartEvent,
|
|
33
|
+
SlashCommandInfo,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export interface ToolExecutionStartEvent {
|
|
37
|
+
type: "tool_execution_start";
|
|
38
|
+
toolCallId: string;
|
|
39
|
+
toolName: string;
|
|
40
|
+
args: unknown;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface ToolExecutionUpdateEvent {
|
|
44
|
+
type: "tool_execution_update";
|
|
45
|
+
toolCallId: string;
|
|
46
|
+
toolName: string;
|
|
47
|
+
args: unknown;
|
|
48
|
+
partialResult: unknown;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface ToolExecutionEndEvent {
|
|
52
|
+
type: "tool_execution_end";
|
|
53
|
+
toolCallId: string;
|
|
54
|
+
toolName: string;
|
|
55
|
+
result: unknown;
|
|
56
|
+
isError: boolean;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface PiSettingsManager {
|
|
60
|
+
reload: () => Promise<void>;
|
|
61
|
+
flush: () => Promise<void>;
|
|
62
|
+
getEnabledModels: () => string[] | undefined;
|
|
63
|
+
setEnabledModels: (patterns: string[] | undefined) => void;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export type PiSlashCommandInfo = SlashCommandInfo;
|
|
67
|
+
export type PiRunMode = "tui" | "rpc" | "json" | "print";
|
|
68
|
+
|
|
69
|
+
function isPiRunMode(value: unknown): value is PiRunMode {
|
|
70
|
+
return (
|
|
71
|
+
value === "tui" ||
|
|
72
|
+
value === "rpc" ||
|
|
73
|
+
value === "json" ||
|
|
74
|
+
value === "print"
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function getExtensionContextMode(ctx: unknown): PiRunMode | undefined {
|
|
79
|
+
const mode =
|
|
80
|
+
typeof ctx === "object" && ctx !== null
|
|
81
|
+
? (ctx as { mode?: unknown }).mode
|
|
82
|
+
: undefined;
|
|
83
|
+
return isPiRunMode(mode) ? mode : undefined;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function isExtensionContextPassiveRunMode(ctx: unknown): boolean {
|
|
87
|
+
const mode = getExtensionContextMode(ctx);
|
|
88
|
+
return mode === "print" || mode === "json";
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function canStartPollingInExtensionContext(ctx: unknown): boolean {
|
|
92
|
+
return !isExtensionContextPassiveRunMode(ctx);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function formatPollingStartBlockedByRunMode(ctx: unknown): string {
|
|
96
|
+
const mode = getExtensionContextMode(ctx);
|
|
97
|
+
return mode
|
|
98
|
+
? `XMPP connection is unavailable in Pi ${mode} mode. Use /xmpp-connect from a long-lived Pi session.`
|
|
99
|
+
: "XMPP connection is unavailable in this Pi run mode.";
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export type PiSendUserMessageOptions = NonNullable<
|
|
103
|
+
Parameters<ExtensionAPI["sendUserMessage"]>[1]
|
|
104
|
+
>;
|
|
105
|
+
|
|
106
|
+
export interface PiExtensionApiRuntimePorts {
|
|
107
|
+
sendUserMessage: ExtensionAPI["sendUserMessage"];
|
|
108
|
+
exec: ExtensionAPI["exec"];
|
|
109
|
+
getCommands: ExtensionAPI["getCommands"];
|
|
110
|
+
getThinkingLevel: ExtensionAPI["getThinkingLevel"];
|
|
111
|
+
setThinkingLevel: ExtensionAPI["setThinkingLevel"];
|
|
112
|
+
setModel: ExtensionAPI["setModel"];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function createExtensionApiRuntimePorts(
|
|
116
|
+
api: Pick<
|
|
117
|
+
ExtensionAPI,
|
|
118
|
+
| "sendUserMessage"
|
|
119
|
+
| "exec"
|
|
120
|
+
| "getCommands"
|
|
121
|
+
| "getThinkingLevel"
|
|
122
|
+
| "setThinkingLevel"
|
|
123
|
+
| "setModel"
|
|
124
|
+
>,
|
|
125
|
+
): PiExtensionApiRuntimePorts {
|
|
126
|
+
return {
|
|
127
|
+
sendUserMessage: (content, options) => api.sendUserMessage(content, options),
|
|
128
|
+
exec: (command, args, options) => api.exec(command, args, options),
|
|
129
|
+
getCommands: () => api.getCommands(),
|
|
130
|
+
getThinkingLevel: () => api.getThinkingLevel(),
|
|
131
|
+
setThinkingLevel: (level) => api.setThinkingLevel(level),
|
|
132
|
+
setModel: (model) => api.setModel(model),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function createSettingsManager(cwd: string): PiSettingsManager {
|
|
137
|
+
return SettingsManager.create(cwd);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function getExtensionContextModel(
|
|
141
|
+
ctx: ExtensionContext,
|
|
142
|
+
): ExtensionContext["model"] {
|
|
143
|
+
return ctx.model;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function getExtensionContextCwd(ctx: ExtensionContext): string {
|
|
147
|
+
return ctx.cwd;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function isExtensionContextIdle(ctx: ExtensionContext): boolean {
|
|
151
|
+
return ctx.isIdle();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function hasExtensionContextPendingMessages(
|
|
155
|
+
ctx: ExtensionContext,
|
|
156
|
+
): boolean {
|
|
157
|
+
return ctx.hasPendingMessages();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function compactExtensionContext(
|
|
161
|
+
ctx: ExtensionContext,
|
|
162
|
+
callbacks: Parameters<ExtensionContext["compact"]>[0],
|
|
163
|
+
): ReturnType<ExtensionContext["compact"]> {
|
|
164
|
+
return ctx.compact(callbacks);
|
|
165
|
+
}
|
package/lib/prompts.ts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XMPP prompt injection helpers
|
|
3
|
+
* Zones: pi agent prompts, xmpp guidance
|
|
4
|
+
* Owns XMPP-specific system prompt suffixes injected into pi agent turns
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Type } from "@sinclair/typebox";
|
|
8
|
+
import type { BeforeAgentStartEvent, ExtensionAPI } from "./pi.ts";
|
|
9
|
+
import { XMPP_PREFIX } from "./routing.ts";
|
|
10
|
+
|
|
11
|
+
const LOCAL_SYSTEM_PROMPT_SUFFIX = `
|
|
12
|
+
|
|
13
|
+
XMPP bridge available. Do not use it from local/TUI prompts unless explicitly asked.`;
|
|
14
|
+
|
|
15
|
+
const XMPP_TURN_SYSTEM_PROMPT_SUFFIX = `
|
|
16
|
+
|
|
17
|
+
XMPP turn note: If context was compacted or you need the pi-xmpp bridge contract, call tool \`xmpp_help\`.`;
|
|
18
|
+
|
|
19
|
+
const XMPP_HELP_TEXT = `--- XMPP BRIDGE HELP ---
|
|
20
|
+
|
|
21
|
+
How to understand XMPP turns:
|
|
22
|
+
- \`[xmpp|from:user@domain]\` marks XMPP origin and sender.
|
|
23
|
+
- \`[room:room@conference]\` indicates a groupchat (MUC) message.
|
|
24
|
+
- \`[nick:nickname]\` is the sender's nickname in a MUC room.
|
|
25
|
+
- Reply to the user's current instruction, not quoted context.
|
|
26
|
+
|
|
27
|
+
How to answer XMPP turns:
|
|
28
|
+
- Reply in concise, scannable text.
|
|
29
|
+
- For generated/requested files, mention the local path.
|
|
30
|
+
|
|
31
|
+
Assistant-authored XMPP actions:
|
|
32
|
+
- Use the \`xmpp_send\` tool to send direct messages or groupchat replies.
|
|
33
|
+
|
|
34
|
+
Debugging pi-xmpp:
|
|
35
|
+
- Inspect \`~/.pi/agent/tmp/xmpp/state.json\` for runtime state and diagnostics.
|
|
36
|
+
- Use \`/xmpp-status\` for compact health information.`;
|
|
37
|
+
|
|
38
|
+
export function getXmppHelpText(): string {
|
|
39
|
+
return XMPP_HELP_TEXT;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function registerXmppHelpTool(pi: ExtensionAPI): void {
|
|
43
|
+
pi.registerTool({
|
|
44
|
+
name: "xmpp_help",
|
|
45
|
+
label: "XMPP Help",
|
|
46
|
+
description:
|
|
47
|
+
"Read pi-xmpp usage guidance for delivery actions, formatting, and debugging.",
|
|
48
|
+
parameters: Type.Object({}),
|
|
49
|
+
async execute() {
|
|
50
|
+
return {
|
|
51
|
+
content: [{ type: "text", text: getXmppHelpText() }],
|
|
52
|
+
details: {},
|
|
53
|
+
};
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function buildXmppBridgeSystemPrompt(options: {
|
|
59
|
+
prompt: string;
|
|
60
|
+
systemPrompt: string;
|
|
61
|
+
xmppPrefix?: string;
|
|
62
|
+
localSystemPromptSuffix: string;
|
|
63
|
+
xmppTurnSystemPromptSuffix: string;
|
|
64
|
+
}): { systemPrompt: string } {
|
|
65
|
+
const xmppPrefix = options.xmppPrefix ?? XMPP_PREFIX;
|
|
66
|
+
const trimmedPrompt = options.prompt.trimStart();
|
|
67
|
+
const isXmppTurn = trimmedPrompt.startsWith(xmppPrefix);
|
|
68
|
+
|
|
69
|
+
const suffix = isXmppTurn
|
|
70
|
+
? `${options.xmppTurnSystemPromptSuffix}\n- The current user message came from XMPP.`
|
|
71
|
+
: options.localSystemPromptSuffix;
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
systemPrompt: options.systemPrompt + suffix,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function createXmppBeforeAgentStartHook(
|
|
79
|
+
options: {
|
|
80
|
+
xmppPrefix?: string;
|
|
81
|
+
localSystemPromptSuffix?: string;
|
|
82
|
+
xmppTurnSystemPromptSuffix?: string;
|
|
83
|
+
} = {},
|
|
84
|
+
): (event: BeforeAgentStartEvent) => { systemPrompt: string } {
|
|
85
|
+
return (event) =>
|
|
86
|
+
buildXmppBridgeSystemPrompt({
|
|
87
|
+
prompt: event.prompt,
|
|
88
|
+
systemPrompt: event.systemPrompt,
|
|
89
|
+
xmppPrefix: options.xmppPrefix,
|
|
90
|
+
localSystemPromptSuffix:
|
|
91
|
+
options.localSystemPromptSuffix ?? LOCAL_SYSTEM_PROMPT_SUFFIX,
|
|
92
|
+
xmppTurnSystemPromptSuffix:
|
|
93
|
+
options.xmppTurnSystemPromptSuffix ?? XMPP_TURN_SYSTEM_PROMPT_SUFFIX,
|
|
94
|
+
});
|
|
95
|
+
}
|
package/lib/queue.ts
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XMPP message queue and turn management
|
|
3
|
+
* Zones: xmpp queue, turn lifecycle, prompt injection
|
|
4
|
+
* Owns incoming message queuing, turn tracking, and deferred prompt dispatch
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { ExtensionContext } from "./pi.ts";
|
|
8
|
+
import type { XmppMessageRoute } from "./routing.ts";
|
|
9
|
+
|
|
10
|
+
export interface XmppTurnContext {
|
|
11
|
+
from: string;
|
|
12
|
+
fromBare: string;
|
|
13
|
+
body: string;
|
|
14
|
+
type: string;
|
|
15
|
+
thread?: string;
|
|
16
|
+
subject?: string;
|
|
17
|
+
isGroup: boolean;
|
|
18
|
+
roomJid?: string;
|
|
19
|
+
senderNick?: string;
|
|
20
|
+
timestamp: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface XmppQueueItem<TContext> {
|
|
24
|
+
id: string;
|
|
25
|
+
order: number;
|
|
26
|
+
turn: XmppTurnContext;
|
|
27
|
+
prompt: string;
|
|
28
|
+
ctx: TContext;
|
|
29
|
+
status: "queued" | "dispatching" | "active" | "done";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface XmppActiveTurnStore {
|
|
33
|
+
has: () => boolean;
|
|
34
|
+
get: () => XmppTurnContext | undefined;
|
|
35
|
+
getChatId: () => string | undefined;
|
|
36
|
+
set: (turn: XmppTurnContext | undefined) => void;
|
|
37
|
+
clear: () => void;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function createXmppActiveTurnStore(): XmppActiveTurnStore {
|
|
41
|
+
let current: XmppTurnContext | undefined;
|
|
42
|
+
return {
|
|
43
|
+
has: () => current !== undefined,
|
|
44
|
+
get: () => current,
|
|
45
|
+
getChatId: () => current?.fromBare,
|
|
46
|
+
set: (turn) => {
|
|
47
|
+
current = turn;
|
|
48
|
+
},
|
|
49
|
+
clear: () => {
|
|
50
|
+
current = undefined;
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface XmppQueueStore<TContext> {
|
|
56
|
+
enqueue: (item: XmppQueueItem<TContext>) => void;
|
|
57
|
+
dequeue: () => XmppQueueItem<TContext> | undefined;
|
|
58
|
+
peek: () => XmppQueueItem<TContext> | undefined;
|
|
59
|
+
getQueuedItems: () => XmppQueueItem<TContext>[];
|
|
60
|
+
remove: (id: string) => void;
|
|
61
|
+
clear: () => void;
|
|
62
|
+
size: () => number;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function createXmppQueueStore<TContext>(): XmppQueueStore<TContext> {
|
|
66
|
+
const items: XmppQueueItem<TContext>[] = [];
|
|
67
|
+
return {
|
|
68
|
+
enqueue: (item) => {
|
|
69
|
+
items.push(item);
|
|
70
|
+
items.sort((a, b) => a.order - b.order);
|
|
71
|
+
},
|
|
72
|
+
dequeue: () => items.shift(),
|
|
73
|
+
peek: () => items[0],
|
|
74
|
+
getQueuedItems: () => [...items],
|
|
75
|
+
remove: (id) => {
|
|
76
|
+
const idx = items.findIndex((i) => i.id === id);
|
|
77
|
+
if (idx >= 0) items.splice(idx, 1);
|
|
78
|
+
},
|
|
79
|
+
clear: () => {
|
|
80
|
+
items.length = 0;
|
|
81
|
+
},
|
|
82
|
+
size: () => items.length,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function formatQueuedXmppItemsStatus<TContext>(
|
|
87
|
+
items: XmppQueueItem<TContext>[],
|
|
88
|
+
): string {
|
|
89
|
+
if (items.length === 0) return "0 queued";
|
|
90
|
+
return items
|
|
91
|
+
.map(
|
|
92
|
+
(i) =>
|
|
93
|
+
`${i.status}: from=${i.turn.fromBare} body="${i.turn.body.slice(0, 40)}"`,
|
|
94
|
+
)
|
|
95
|
+
.join("; ");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function buildXmppTurnPrompt(
|
|
99
|
+
turn: XmppTurnContext,
|
|
100
|
+
options?: {
|
|
101
|
+
includeThread?: boolean;
|
|
102
|
+
extraContext?: string;
|
|
103
|
+
},
|
|
104
|
+
): string {
|
|
105
|
+
const parts: string[] = [];
|
|
106
|
+
parts.push(`[xmpp|from:${turn.from}]`);
|
|
107
|
+
|
|
108
|
+
if (turn.isGroup && turn.roomJid) {
|
|
109
|
+
parts.push(`[room:${turn.roomJid}]`);
|
|
110
|
+
if (turn.senderNick) parts.push(`[nick:${turn.senderNick}]`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (turn.subject) {
|
|
114
|
+
parts.push(`[subject:${turn.subject}]`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (options?.includeThread && turn.thread) {
|
|
118
|
+
parts.push(`[thread:${turn.thread}]`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (options?.extraContext) {
|
|
122
|
+
parts.push(`[context:${options.extraContext}]`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
parts.push("");
|
|
126
|
+
parts.push(turn.body);
|
|
127
|
+
|
|
128
|
+
return parts.join("\n");
|
|
129
|
+
}
|