pikiclaw 0.3.17 → 0.3.18
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/dist/agent/drivers/codex.js +36 -29
- package/dist/bot/bot.js +103 -3
- package/dist/channels/feishu/bot.js +8 -29
- package/dist/channels/telegram/bot.js +15 -31
- package/dist/channels/weixin/bot.js +1 -6
- package/dist/core/process-control.js +0 -12
- package/dist/dashboard/routes/config.js +1 -5
- package/dist/dashboard/routes/sessions.js +64 -1
- package/dist/dashboard/session-control.js +59 -0
- package/package.json +2 -2
- package/dist/bot/human-loop-codex.js +0 -26
|
@@ -432,36 +432,43 @@ function overlayCodexManagedPreview(workdir, sessionId, richMessages) {
|
|
|
432
432
|
};
|
|
433
433
|
return merged;
|
|
434
434
|
}
|
|
435
|
-
function
|
|
435
|
+
function toAgentInteraction(method, params, requestId) {
|
|
436
436
|
if (method === 'item/tool/requestUserInput') {
|
|
437
|
-
const
|
|
437
|
+
const raw = Array.isArray(params?.questions) ? params.questions : [];
|
|
438
|
+
const questions = raw
|
|
439
|
+
.map((q) => ({
|
|
440
|
+
id: String(q?.id || ''),
|
|
441
|
+
header: String(q?.header || '') || 'Question',
|
|
442
|
+
prompt: String(q?.question || ''),
|
|
443
|
+
options: Array.isArray(q?.options)
|
|
444
|
+
? q.options.map((o) => ({
|
|
445
|
+
label: String(o?.label || ''),
|
|
446
|
+
description: String(o?.description || ''),
|
|
447
|
+
value: String(o?.label || ''),
|
|
448
|
+
}))
|
|
449
|
+
: null,
|
|
450
|
+
allowFreeform: !!q?.isOther || !Array.isArray(q?.options) || !q.options.length,
|
|
451
|
+
secret: !!q?.isSecret,
|
|
452
|
+
allowEmpty: true,
|
|
453
|
+
}))
|
|
454
|
+
.filter((q) => q.id && q.prompt);
|
|
438
455
|
return {
|
|
439
|
-
kind: '
|
|
440
|
-
requestId,
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
question: String(question?.question || ''),
|
|
448
|
-
isOther: !!question?.isOther,
|
|
449
|
-
isSecret: !!question?.isSecret,
|
|
450
|
-
options: Array.isArray(question?.options)
|
|
451
|
-
? question.options.map((option) => ({
|
|
452
|
-
label: String(option?.label || ''),
|
|
453
|
-
description: String(option?.description || ''),
|
|
454
|
-
}))
|
|
455
|
-
: null,
|
|
456
|
-
})).filter((question) => question.id && question.question),
|
|
456
|
+
kind: 'user-input',
|
|
457
|
+
id: requestId,
|
|
458
|
+
title: 'User Input Required',
|
|
459
|
+
hint: 'Use the buttons when available. Reply with text when prompted.',
|
|
460
|
+
questions,
|
|
461
|
+
resolveWith: (answers) => ({
|
|
462
|
+
answers: Object.fromEntries(Object.entries(answers).map(([id, vals]) => [id, { answers: vals }])),
|
|
463
|
+
}),
|
|
457
464
|
};
|
|
458
465
|
}
|
|
459
466
|
return null;
|
|
460
467
|
}
|
|
461
|
-
function
|
|
468
|
+
function defaultAgentInteractionResponse(interaction) {
|
|
462
469
|
const answers = {};
|
|
463
|
-
for (const
|
|
464
|
-
answers[
|
|
470
|
+
for (const q of interaction.questions)
|
|
471
|
+
answers[q.id] = { answers: [] };
|
|
465
472
|
return { answers };
|
|
466
473
|
}
|
|
467
474
|
function defaultCodexServerRequestResponse(method) {
|
|
@@ -836,22 +843,22 @@ function handleTurnPlanUpdated(params, s, emit) {
|
|
|
836
843
|
// Stream request handler (extracted from doCodexStream)
|
|
837
844
|
// ---------------------------------------------------------------------------
|
|
838
845
|
async function handleCodexRequest(method, params, requestId, s, opts, emit) {
|
|
839
|
-
const interaction =
|
|
846
|
+
const interaction = toAgentInteraction(method, params, requestId);
|
|
840
847
|
if (!interaction)
|
|
841
848
|
return defaultCodexServerRequestResponse(method);
|
|
842
|
-
pushRecentActivity(s.recentNarrative, interaction.kind === '
|
|
849
|
+
pushRecentActivity(s.recentNarrative, interaction.kind === 'user-input' ? 'Waiting for user input' : 'Waiting for approval');
|
|
843
850
|
emit();
|
|
844
851
|
try {
|
|
845
|
-
if (opts.
|
|
846
|
-
const response = await opts.
|
|
847
|
-
return response ??
|
|
852
|
+
if (opts.onInteraction) {
|
|
853
|
+
const response = await opts.onInteraction(interaction);
|
|
854
|
+
return response ?? defaultAgentInteractionResponse(interaction);
|
|
848
855
|
}
|
|
849
856
|
}
|
|
850
857
|
catch (error) {
|
|
851
858
|
pushRecentActivity(s.recentFailures, `Human input failed: ${shortValue(error?.message || error, 120)}`, 4);
|
|
852
859
|
emit();
|
|
853
860
|
}
|
|
854
|
-
return
|
|
861
|
+
return defaultAgentInteractionResponse(interaction);
|
|
855
862
|
}
|
|
856
863
|
// ---------------------------------------------------------------------------
|
|
857
864
|
// Stream via app-server
|
package/dist/bot/bot.js
CHANGED
|
@@ -279,6 +279,26 @@ export class Bot {
|
|
|
279
279
|
}
|
|
280
280
|
break;
|
|
281
281
|
}
|
|
282
|
+
case 'interaction': {
|
|
283
|
+
const snap = this.streamSnapshots.get(sessionKey);
|
|
284
|
+
if (snap) {
|
|
285
|
+
const list = snap.interactions || [];
|
|
286
|
+
list.push(event.interaction);
|
|
287
|
+
snap.interactions = list;
|
|
288
|
+
snap.updatedAt = now;
|
|
289
|
+
}
|
|
290
|
+
break;
|
|
291
|
+
}
|
|
292
|
+
case 'interaction-resolved': {
|
|
293
|
+
const snap = this.streamSnapshots.get(sessionKey);
|
|
294
|
+
if (snap?.interactions) {
|
|
295
|
+
snap.interactions = snap.interactions.filter(i => i.promptId !== event.promptId);
|
|
296
|
+
if (!snap.interactions.length)
|
|
297
|
+
delete snap.interactions;
|
|
298
|
+
snap.updatedAt = now;
|
|
299
|
+
}
|
|
300
|
+
break;
|
|
301
|
+
}
|
|
282
302
|
}
|
|
283
303
|
// Push to dashboard SSE — throttle text events, push everything else immediately
|
|
284
304
|
try {
|
|
@@ -903,6 +923,7 @@ export class Bot {
|
|
|
903
923
|
this.humanLoopPrompts.delete(promptId);
|
|
904
924
|
this.removeHumanLoopPromptFromChat(prompt.chatId, promptId);
|
|
905
925
|
prompt.resolve(buildHumanLoopResponse(prompt));
|
|
926
|
+
this.emitInteractionResolved(prompt.taskId, promptId);
|
|
906
927
|
return prompt;
|
|
907
928
|
}
|
|
908
929
|
clearHumanLoopPrompt(promptId, error) {
|
|
@@ -913,8 +934,14 @@ export class Bot {
|
|
|
913
934
|
this.removeHumanLoopPromptFromChat(prompt.chatId, promptId);
|
|
914
935
|
if (error)
|
|
915
936
|
prompt.reject(error);
|
|
937
|
+
this.emitInteractionResolved(prompt.taskId, promptId);
|
|
916
938
|
return prompt;
|
|
917
939
|
}
|
|
940
|
+
emitInteractionResolved(taskId, promptId) {
|
|
941
|
+
const task = this.activeTasks.get(taskId);
|
|
942
|
+
if (task)
|
|
943
|
+
this.emitStream(task.sessionKey, { type: 'interaction-resolved', promptId });
|
|
944
|
+
}
|
|
918
945
|
humanLoopSelectOption(promptId, optionValue, opts = {}) {
|
|
919
946
|
const prompt = this.humanLoopPrompts.get(promptId) || null;
|
|
920
947
|
if (!prompt)
|
|
@@ -963,6 +990,79 @@ export class Bot {
|
|
|
963
990
|
else
|
|
964
991
|
this.humanLoopPromptIdsByChat.delete(chatKey);
|
|
965
992
|
}
|
|
993
|
+
/**
|
|
994
|
+
* Create an interaction handler that bridges agent requests to the human-loop
|
|
995
|
+
* state machine and pushes SSE events to the dashboard.
|
|
996
|
+
*
|
|
997
|
+
* IM channel subclasses override `renderInteractionPrompt()` to render
|
|
998
|
+
* buttons/cards in their native UI. Dashboard clients receive the
|
|
999
|
+
* `interaction` SSE event and respond via REST.
|
|
1000
|
+
*/
|
|
1001
|
+
createInteractionHandler(chatId, taskId, sessionKey) {
|
|
1002
|
+
return async (request) => {
|
|
1003
|
+
const active = this.beginHumanLoopPrompt({
|
|
1004
|
+
taskId,
|
|
1005
|
+
chatId,
|
|
1006
|
+
title: request.title,
|
|
1007
|
+
hint: request.hint,
|
|
1008
|
+
questions: request.questions,
|
|
1009
|
+
resolveWith: request.resolveWith,
|
|
1010
|
+
});
|
|
1011
|
+
const interactionSnapshot = {
|
|
1012
|
+
promptId: active.prompt.promptId,
|
|
1013
|
+
kind: request.kind,
|
|
1014
|
+
title: request.title,
|
|
1015
|
+
hint: request.hint,
|
|
1016
|
+
questions: request.questions,
|
|
1017
|
+
};
|
|
1018
|
+
this.emitStream(sessionKey, { type: 'interaction', taskId, interaction: interactionSnapshot });
|
|
1019
|
+
try {
|
|
1020
|
+
await this.renderInteractionPrompt(active.prompt, chatId);
|
|
1021
|
+
}
|
|
1022
|
+
catch (error) {
|
|
1023
|
+
this.humanLoopCancel(active.prompt.promptId, error?.message || 'Failed to send prompt.');
|
|
1024
|
+
throw error;
|
|
1025
|
+
}
|
|
1026
|
+
return active.result;
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
/**
|
|
1030
|
+
* Render an interaction prompt in the IM channel.
|
|
1031
|
+
* Override in channel subclasses (Telegram, Feishu, etc.).
|
|
1032
|
+
* Dashboard-only sessions (chatId='dashboard') are a no-op by default.
|
|
1033
|
+
*/
|
|
1034
|
+
async renderInteractionPrompt(_prompt, _chatId) {
|
|
1035
|
+
// Default: no-op (dashboard-only sessions use SSE events instead)
|
|
1036
|
+
}
|
|
1037
|
+
// ---- Public interaction API (used by dashboard routes) --------------------
|
|
1038
|
+
/** Respond to a pending interaction prompt with a selected option. */
|
|
1039
|
+
interactionSelectOption(promptId, optionValue, opts) {
|
|
1040
|
+
return this.humanLoopSelectOption(promptId, optionValue, opts);
|
|
1041
|
+
}
|
|
1042
|
+
/** Submit freeform text to a pending interaction prompt. */
|
|
1043
|
+
interactionSubmitText(promptId, text) {
|
|
1044
|
+
const prompt = this.humanLoopPrompt(promptId);
|
|
1045
|
+
if (!prompt)
|
|
1046
|
+
return null;
|
|
1047
|
+
if (!isHumanLoopAwaitingText(prompt))
|
|
1048
|
+
return null;
|
|
1049
|
+
const result = setHumanLoopText(prompt, text);
|
|
1050
|
+
if (result.completed)
|
|
1051
|
+
this.resolveHumanLoopPrompt(prompt.promptId);
|
|
1052
|
+
return { prompt, ...result };
|
|
1053
|
+
}
|
|
1054
|
+
/** Skip the current question in a pending interaction prompt. */
|
|
1055
|
+
interactionSkip(promptId) {
|
|
1056
|
+
return this.humanLoopSkip(promptId);
|
|
1057
|
+
}
|
|
1058
|
+
/** Cancel a pending interaction prompt. */
|
|
1059
|
+
interactionCancel(promptId, reason = 'Cancelled from dashboard.') {
|
|
1060
|
+
return this.humanLoopCancel(promptId, reason);
|
|
1061
|
+
}
|
|
1062
|
+
/** Get a specific interaction prompt by ID. */
|
|
1063
|
+
interactionPrompt(promptId) {
|
|
1064
|
+
return this.humanLoopPrompt(promptId);
|
|
1065
|
+
}
|
|
966
1066
|
selectedSession(chatId) {
|
|
967
1067
|
return this.getSelectedSession(this.chat(chatId));
|
|
968
1068
|
}
|
|
@@ -1003,7 +1103,7 @@ export class Bot {
|
|
|
1003
1103
|
const result = await this.runStream(prompt, session, attachments, (text, thinking, activity, meta, plan) => {
|
|
1004
1104
|
opts.onText?.(text, thinking, activity, meta, plan);
|
|
1005
1105
|
this.emitStream(currentSessionKey(), { type: 'text', text, thinking, activity, plan });
|
|
1006
|
-
}, undefined, undefined, abortController.signal);
|
|
1106
|
+
}, undefined, undefined, abortController.signal, this.createInteractionHandler(opts.chatId ?? 'dashboard', taskId, currentSessionKey()));
|
|
1007
1107
|
this.emitStream(currentSessionKey(), {
|
|
1008
1108
|
type: 'done',
|
|
1009
1109
|
taskId,
|
|
@@ -1272,7 +1372,7 @@ export class Bot {
|
|
|
1272
1372
|
if (!opts.initial)
|
|
1273
1373
|
this.onManagedConfigChange(config, opts);
|
|
1274
1374
|
}
|
|
1275
|
-
async runStream(prompt, cs, attachments, onText, systemPrompt, mcpSendFile, abortSignal,
|
|
1375
|
+
async runStream(prompt, cs, attachments, onText, systemPrompt, mcpSendFile, abortSignal, onInteraction, onSteerReady, onCodexTurnReady) {
|
|
1276
1376
|
const resolvedModel = cs.modelId || this.modelForAgent(cs.agent);
|
|
1277
1377
|
const agentConfig = this.agentConfigs[cs.agent] || {};
|
|
1278
1378
|
const resolvedThinkingEffort = ('thinkingEffort' in cs && typeof cs.thinkingEffort === 'string' && cs.thinkingEffort.trim())
|
|
@@ -1359,7 +1459,7 @@ export class Bot {
|
|
|
1359
1459
|
// MCP bridge
|
|
1360
1460
|
mcpSendFile,
|
|
1361
1461
|
abortSignal,
|
|
1362
|
-
|
|
1462
|
+
onInteraction,
|
|
1363
1463
|
onSteerReady,
|
|
1364
1464
|
onCodexTurnReady,
|
|
1365
1465
|
};
|
|
@@ -17,9 +17,8 @@ import { SKILL_CMD_PREFIX, } from '../../bot/menu.js';
|
|
|
17
17
|
import { getStartData, getSessionsPageData, getModelsListData, getSessionTurnPreviewData, getStatusDataAsync, getHostDataSync, resolveSkillPrompt, } from '../../bot/commands.js';
|
|
18
18
|
import { buildAgentsCommandView, buildModelsCommandView, buildModeCommandView, buildSessionsCommandView, buildSkillsCommandView, decodeCommandAction, executeCommandAction, } from '../../bot/command-ui.js';
|
|
19
19
|
import { LivePreview } from '../telegram/live-preview.js';
|
|
20
|
-
import {
|
|
20
|
+
import { registerProcessRuntime, requestProcessRestart, } from '../../core/process-control.js';
|
|
21
21
|
import { feishuPreviewRenderer, buildInitialPreviewMarkdown, buildHumanLoopPromptMarkdown, buildFinalReplyRender, renderCommandNotice, renderCommandSelectionCard, renderSessionTurnMarkdown, renderStart, renderStatus, renderHost, buildSwitchWorkdirCard, resolveFeishuRegisteredPath, } from './render.js';
|
|
22
|
-
import { buildCodexHumanLoopPrompt } from '../../bot/human-loop-codex.js';
|
|
23
22
|
import { currentHumanLoopQuestion, humanLoopOptionSelected } from '../../bot/human-loop.js';
|
|
24
23
|
import { FeishuChannel } from './channel.js';
|
|
25
24
|
import { splitText, supportsChannelCapability } from '../base.js';
|
|
@@ -384,11 +383,6 @@ export class FeishuBot extends Bot {
|
|
|
384
383
|
await ctx.channel.sendCard(ctx.chatId, view);
|
|
385
384
|
}
|
|
386
385
|
async cmdRestart(ctx) {
|
|
387
|
-
const activeTasks = getActiveTaskCount();
|
|
388
|
-
if (activeTasks > 0) {
|
|
389
|
-
await ctx.reply(`⚠ ${formatActiveTaskRestartError(activeTasks)}`);
|
|
390
|
-
return;
|
|
391
|
-
}
|
|
392
386
|
await ctx.reply('**Restarting pikiclaw...**\n\nPulling latest version. The bot will be back shortly.');
|
|
393
387
|
void requestProcessRestart({ log: msg => this.log(msg) });
|
|
394
388
|
}
|
|
@@ -474,27 +468,12 @@ export class FeishuBot extends Bot {
|
|
|
474
468
|
keyboard: { rows: [] },
|
|
475
469
|
}).catch(() => { });
|
|
476
470
|
}
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
...blueprint,
|
|
484
|
-
});
|
|
485
|
-
try {
|
|
486
|
-
const sent = await ctx.reply(buildHumanLoopPromptMarkdown(active.prompt), {
|
|
487
|
-
keyboard: this.buildHumanLoopKeyboard(active.prompt.promptId),
|
|
488
|
-
});
|
|
489
|
-
if (sent)
|
|
490
|
-
this.registerHumanLoopMessage(active.prompt.promptId, sent);
|
|
491
|
-
}
|
|
492
|
-
catch (error) {
|
|
493
|
-
this.humanLoopCancel(active.prompt.promptId, error?.message || 'Failed to send prompt.');
|
|
494
|
-
throw error;
|
|
495
|
-
}
|
|
496
|
-
return active.result;
|
|
497
|
-
};
|
|
471
|
+
async renderInteractionPrompt(prompt, chatId) {
|
|
472
|
+
const sent = await this.channel.send(chatId, buildHumanLoopPromptMarkdown(prompt), {
|
|
473
|
+
keyboard: this.buildHumanLoopKeyboard(prompt.promptId),
|
|
474
|
+
});
|
|
475
|
+
if (sent)
|
|
476
|
+
this.registerHumanLoopMessage(prompt.promptId, sent);
|
|
498
477
|
}
|
|
499
478
|
async safeSetMessageReaction(chatId, messageId, reactions) {
|
|
500
479
|
if (!supportsChannelCapability(this.channel, 'messageReactions'))
|
|
@@ -640,7 +619,7 @@ export class FeishuBot extends Bot {
|
|
|
640
619
|
const mcpSendFile = this.createMcpSendFileCallback(ctx);
|
|
641
620
|
const result = await this.runStream(prompt, session, files, (nextText, nextThinking, nextActivity = '', meta, plan) => {
|
|
642
621
|
livePreview?.update(nextText, nextThinking, nextActivity, meta, plan);
|
|
643
|
-
}, undefined, mcpSendFile, abortController.signal, this.
|
|
622
|
+
}, undefined, mcpSendFile, abortController.signal, this.createInteractionHandler(ctx.chatId, taskId, session.key), (steer) => {
|
|
644
623
|
const currentTask = this.activeTasks.get(taskId);
|
|
645
624
|
if (!currentTask || currentTask.cancelled || currentTask.status !== 'running')
|
|
646
625
|
return;
|
|
@@ -18,9 +18,8 @@ import { getStartData, getStatusDataAsync, getHostDataSync, getSessionTurnPrevie
|
|
|
18
18
|
import { buildAgentsCommandView, buildModelsCommandView, buildModeCommandView, buildSessionsCommandView, buildSkillsCommandView, decodeCommandAction, executeCommandAction, } from '../../bot/command-ui.js';
|
|
19
19
|
import { buildSwitchWorkdirView, resolveRegisteredPath } from './directory.js';
|
|
20
20
|
import { LivePreview } from './live-preview.js';
|
|
21
|
-
import {
|
|
21
|
+
import { registerProcessRuntime, buildRestartCommand, requestProcessRestart, } from '../../core/process-control.js';
|
|
22
22
|
import { buildInitialPreviewHtml, buildHumanLoopPromptHtml, buildStreamPreviewHtml, buildFinalReplyRender, escapeHtml, formatMenuLines, formatProviderUsageLines, renderCommandNoticeHtml, renderCommandSelectionHtml, renderCommandSelectionKeyboard, renderSessionTurnHtml, truncateMiddle, } from './render.js';
|
|
23
|
-
import { buildCodexHumanLoopPrompt } from '../../bot/human-loop-codex.js';
|
|
24
23
|
import { currentHumanLoopQuestion, humanLoopOptionSelected } from '../../bot/human-loop.js';
|
|
25
24
|
import { TelegramChannel } from './channel.js';
|
|
26
25
|
import { splitText, supportsChannelCapability } from '../base.js';
|
|
@@ -348,11 +347,6 @@ export class TelegramBot extends Bot {
|
|
|
348
347
|
await this.sendCommandView(ctx, buildModeCommandView(this, ctx.chatId));
|
|
349
348
|
}
|
|
350
349
|
async cmdRestart(ctx) {
|
|
351
|
-
const activeTasks = getActiveTaskCount();
|
|
352
|
-
if (activeTasks > 0) {
|
|
353
|
-
await ctx.reply(`⚠ ${formatActiveTaskRestartError(activeTasks)}`, { parseMode: 'HTML' });
|
|
354
|
-
return;
|
|
355
|
-
}
|
|
356
350
|
await ctx.reply(`<b>Restarting pikiclaw...</b>\n\n` +
|
|
357
351
|
`The bot will be back shortly.`, { parseMode: 'HTML' });
|
|
358
352
|
void requestProcessRestart({ log: msg => this.log(msg) });
|
|
@@ -418,30 +412,18 @@ export class TelegramBot extends Bot {
|
|
|
418
412
|
keyboard: { inline_keyboard: [] },
|
|
419
413
|
}).catch(() => { });
|
|
420
414
|
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
const sent = await ctx.reply(buildHumanLoopPromptHtml(active.prompt), {
|
|
431
|
-
parseMode: 'HTML',
|
|
432
|
-
messageThreadId,
|
|
433
|
-
keyboard: this.buildHumanLoopKeyboard(active.prompt.promptId),
|
|
434
|
-
});
|
|
435
|
-
if (typeof sent === 'number')
|
|
436
|
-
this.registerHumanLoopMessage(active.prompt.promptId, sent);
|
|
437
|
-
}
|
|
438
|
-
catch (error) {
|
|
439
|
-
this.humanLoopCancel(active.prompt.promptId, error?.message || 'Failed to send prompt.');
|
|
440
|
-
throw error;
|
|
441
|
-
}
|
|
442
|
-
return active.result;
|
|
443
|
-
};
|
|
415
|
+
async renderInteractionPrompt(prompt, chatId) {
|
|
416
|
+
const messageThreadId = this.interactionThreadIds.get(prompt.taskId);
|
|
417
|
+
const sent = await this.channel.send(chatId, buildHumanLoopPromptHtml(prompt), {
|
|
418
|
+
parseMode: 'HTML',
|
|
419
|
+
messageThreadId,
|
|
420
|
+
keyboard: this.buildHumanLoopKeyboard(prompt.promptId),
|
|
421
|
+
});
|
|
422
|
+
if (typeof sent === 'number')
|
|
423
|
+
this.registerHumanLoopMessage(prompt.promptId, sent);
|
|
444
424
|
}
|
|
425
|
+
/** Cache the messageThreadId per task so renderInteractionPrompt can use it. */
|
|
426
|
+
interactionThreadIds = new Map();
|
|
445
427
|
// ---- streaming bridge -----------------------------------------------------
|
|
446
428
|
async handleMessage(msg, ctx) {
|
|
447
429
|
const text = msg.text.trim();
|
|
@@ -581,9 +563,10 @@ export class TelegramBot extends Bot {
|
|
|
581
563
|
}
|
|
582
564
|
// MCP sendFile callback: sends files to IM in real-time during the stream
|
|
583
565
|
const mcpSendFile = this.createMcpSendFileCallback(ctx, messageThreadId);
|
|
566
|
+
this.interactionThreadIds.set(taskId, messageThreadId);
|
|
584
567
|
const result = await this.runStream(prompt, session, files, (nextText, nextThinking, nextActivity = '', meta, plan) => {
|
|
585
568
|
livePreview?.update(nextText, nextThinking, nextActivity, meta, plan);
|
|
586
|
-
}, undefined, mcpSendFile, abortController.signal, this.
|
|
569
|
+
}, undefined, mcpSendFile, abortController.signal, this.createInteractionHandler(ctx.chatId, taskId, session.key), (steer) => {
|
|
587
570
|
const currentTask = this.activeTasks.get(taskId);
|
|
588
571
|
if (!currentTask || currentTask.cancelled || currentTask.status !== 'running')
|
|
589
572
|
return;
|
|
@@ -631,6 +614,7 @@ export class TelegramBot extends Bot {
|
|
|
631
614
|
}
|
|
632
615
|
finally {
|
|
633
616
|
livePreview?.dispose();
|
|
617
|
+
this.interactionThreadIds.delete(taskId);
|
|
634
618
|
this.finishTask(taskId);
|
|
635
619
|
this.syncSelectedChats(session);
|
|
636
620
|
}
|
|
@@ -7,7 +7,7 @@ import path from 'node:path';
|
|
|
7
7
|
import { Bot, buildPrompt, fmtUptime, fmtBytes, normalizeAgent, parseAllowedChatIds, } from '../../bot/bot.js';
|
|
8
8
|
import { BOT_SHUTDOWN_FORCE_EXIT_MS, buildSessionTaskId } from '../../bot/orchestration.js';
|
|
9
9
|
import { shutdownAllDrivers } from '../../agent/driver.js';
|
|
10
|
-
import {
|
|
10
|
+
import { registerProcessRuntime, requestProcessRestart, } from '../../core/process-control.js';
|
|
11
11
|
import { getStatusDataAsync, getHostDataSync, getAgentsListData, getSkillsListData, getModelsListData, getSessionsPageData, getStartData, } from '../../bot/commands.js';
|
|
12
12
|
import { WeixinChannel } from './channel.js';
|
|
13
13
|
import { getActiveUserConfig } from '../../core/config/user-config.js';
|
|
@@ -386,11 +386,6 @@ export class WeixinBot extends Bot {
|
|
|
386
386
|
await ctx.reply(`Stopped: ${parts.join(', ')}.`);
|
|
387
387
|
}
|
|
388
388
|
async cmdRestart(ctx) {
|
|
389
|
-
const activeTasks = getActiveTaskCount();
|
|
390
|
-
if (activeTasks > 0) {
|
|
391
|
-
await ctx.reply(formatActiveTaskRestartError(activeTasks));
|
|
392
|
-
return;
|
|
393
|
-
}
|
|
394
389
|
await ctx.reply('Restarting pikiclaw...');
|
|
395
390
|
void requestProcessRestart({ log: msg => this.log(msg) });
|
|
396
391
|
}
|
|
@@ -87,9 +87,6 @@ export function getActiveTaskCount() {
|
|
|
87
87
|
}
|
|
88
88
|
return total;
|
|
89
89
|
}
|
|
90
|
-
export function formatActiveTaskRestartError(activeTasks) {
|
|
91
|
-
return `${activeTasks} task(s) still running. Wait for them to finish or try again.`;
|
|
92
|
-
}
|
|
93
90
|
export function createRestartStateFilePath(ownerPid = process.pid) {
|
|
94
91
|
const dir = path.join(os.tmpdir(), 'pikiclaw');
|
|
95
92
|
fs.mkdirSync(dir, { recursive: true });
|
|
@@ -188,15 +185,6 @@ function spawnReplacementProcess(bin, args, env, log) {
|
|
|
188
185
|
return child;
|
|
189
186
|
}
|
|
190
187
|
export async function requestProcessRestart(opts = {}) {
|
|
191
|
-
const activeTasks = getActiveTaskCount();
|
|
192
|
-
if (activeTasks > 0) {
|
|
193
|
-
return {
|
|
194
|
-
ok: false,
|
|
195
|
-
restarting: false,
|
|
196
|
-
error: formatActiveTaskRestartError(activeTasks),
|
|
197
|
-
activeTasks,
|
|
198
|
-
};
|
|
199
|
-
}
|
|
200
188
|
if (restartInFlight) {
|
|
201
189
|
return {
|
|
202
190
|
ok: true,
|
|
@@ -12,7 +12,7 @@ import { validateFeishuConfig, validateTelegramConfig, validateWeixinConfig } fr
|
|
|
12
12
|
import { resolveGuiIntegrationConfig } from '../../agent/mcp/bridge.js';
|
|
13
13
|
import { normalizeWeixinBaseUrl, startWeixinQrLogin, waitForWeixinQrLogin, } from '../../channels/weixin/api.js';
|
|
14
14
|
import { getManagedBrowserStatus, launchManagedBrowserSetup, } from '../../browser-profile.js';
|
|
15
|
-
import {
|
|
15
|
+
import { requestProcessRestart, } from '../../core/process-control.js';
|
|
16
16
|
import { checkPermissions, detectHostTerminalApp, installAppium, isAppiumInstalled, isManagedAppiumRunning, isValidPermissionKey, requestPermission, startManagedAppium, stopManagedAppium, } from '../platform.js';
|
|
17
17
|
import { VERSION } from '../../core/version.js';
|
|
18
18
|
import { runtime } from '../runtime.js';
|
|
@@ -258,10 +258,6 @@ app.post('/api/open-preferences', async (c) => {
|
|
|
258
258
|
});
|
|
259
259
|
// Restart process
|
|
260
260
|
app.post('/api/restart', (c) => {
|
|
261
|
-
const activeTasks = getActiveTaskCount();
|
|
262
|
-
if (activeTasks > 0) {
|
|
263
|
-
return c.json({ ok: false, error: formatActiveTaskRestartError(activeTasks) }, 409);
|
|
264
|
-
}
|
|
265
261
|
setTimeout(() => {
|
|
266
262
|
void requestProcessRestart({ log: message => runtime.log(message) });
|
|
267
263
|
}, 50);
|
|
@@ -8,7 +8,7 @@ import path from 'node:path';
|
|
|
8
8
|
import { loadUserConfig } from '../../core/config/user-config.js';
|
|
9
9
|
import { listAgents, listSkills } from '../../agent/index.js';
|
|
10
10
|
import { getSessionStatusForBot } from '../../bot/session-status.js';
|
|
11
|
-
import { cancelSessionTask, getSessionStreamState, queueDashboardSessionTask, steerSessionTask, } from '../session-control.js';
|
|
11
|
+
import { cancelSessionTask, getSessionStreamState, queueDashboardSessionTask, steerSessionTask, interactionSelectOption, interactionSubmitText, interactionSkip, interactionCancel, getInteractionPrompt, } from '../session-control.js';
|
|
12
12
|
import { querySessions, querySessionTail, querySessionMessages, getWorkspaceOverviews, updateSession, linkSessions, buildMigrationContext, exportSession, importSession, loadWorkspaces, addWorkspace, removeWorkspace, updateWorkspace, } from '../../bot/session-hub.js';
|
|
13
13
|
import { DASHBOARD_PAGINATION } from '../../core/constants.js';
|
|
14
14
|
import { runtime } from '../runtime.js';
|
|
@@ -505,4 +505,67 @@ app.post('/api/session-hub/session/steer', async (c) => {
|
|
|
505
505
|
return c.json({ ok: false, error: e.message }, 500);
|
|
506
506
|
}
|
|
507
507
|
});
|
|
508
|
+
// ==========================================================================
|
|
509
|
+
// Interaction prompts (human-in-the-loop)
|
|
510
|
+
// ==========================================================================
|
|
511
|
+
/** GET /api/interaction/:promptId — Get interaction prompt state. */
|
|
512
|
+
app.get('/api/interaction/:promptId', (c) => {
|
|
513
|
+
const { promptId } = c.req.param();
|
|
514
|
+
const result = getInteractionPrompt(promptId);
|
|
515
|
+
return c.json(result, result.ok ? 200 : 503);
|
|
516
|
+
});
|
|
517
|
+
/** POST /api/interaction/:promptId/select — Select an option. */
|
|
518
|
+
app.post('/api/interaction/:promptId/select', async (c) => {
|
|
519
|
+
try {
|
|
520
|
+
const { promptId } = c.req.param();
|
|
521
|
+
const body = await c.req.json();
|
|
522
|
+
const { value, requestFreeform } = body || {};
|
|
523
|
+
if (!value && !requestFreeform) {
|
|
524
|
+
return c.json({ ok: false, error: 'value is required' }, 400);
|
|
525
|
+
}
|
|
526
|
+
const result = interactionSelectOption(promptId, value || '__other__', { requestFreeform: !!requestFreeform });
|
|
527
|
+
return c.json(result, result.ok ? 200 : (result.error === 'Bot is not running' ? 503 : 404));
|
|
528
|
+
}
|
|
529
|
+
catch (e) {
|
|
530
|
+
return c.json({ ok: false, error: e.message }, 500);
|
|
531
|
+
}
|
|
532
|
+
});
|
|
533
|
+
/** POST /api/interaction/:promptId/text — Submit freeform text. */
|
|
534
|
+
app.post('/api/interaction/:promptId/text', async (c) => {
|
|
535
|
+
try {
|
|
536
|
+
const { promptId } = c.req.param();
|
|
537
|
+
const body = await c.req.json();
|
|
538
|
+
const { text } = body || {};
|
|
539
|
+
if (typeof text !== 'string') {
|
|
540
|
+
return c.json({ ok: false, error: 'text is required' }, 400);
|
|
541
|
+
}
|
|
542
|
+
const result = interactionSubmitText(promptId, text);
|
|
543
|
+
return c.json(result, result.ok ? 200 : (result.error === 'Bot is not running' ? 503 : 404));
|
|
544
|
+
}
|
|
545
|
+
catch (e) {
|
|
546
|
+
return c.json({ ok: false, error: e.message }, 500);
|
|
547
|
+
}
|
|
548
|
+
});
|
|
549
|
+
/** POST /api/interaction/:promptId/skip — Skip current question. */
|
|
550
|
+
app.post('/api/interaction/:promptId/skip', async (c) => {
|
|
551
|
+
try {
|
|
552
|
+
const { promptId } = c.req.param();
|
|
553
|
+
const result = interactionSkip(promptId);
|
|
554
|
+
return c.json(result, result.ok ? 200 : (result.error === 'Bot is not running' ? 503 : 404));
|
|
555
|
+
}
|
|
556
|
+
catch (e) {
|
|
557
|
+
return c.json({ ok: false, error: e.message }, 500);
|
|
558
|
+
}
|
|
559
|
+
});
|
|
560
|
+
/** POST /api/interaction/:promptId/cancel — Cancel interaction prompt. */
|
|
561
|
+
app.post('/api/interaction/:promptId/cancel', async (c) => {
|
|
562
|
+
try {
|
|
563
|
+
const { promptId } = c.req.param();
|
|
564
|
+
const result = interactionCancel(promptId);
|
|
565
|
+
return c.json(result, result.ok ? 200 : (result.error === 'Bot is not running' ? 503 : 404));
|
|
566
|
+
}
|
|
567
|
+
catch (e) {
|
|
568
|
+
return c.json({ ok: false, error: e.message }, 500);
|
|
569
|
+
}
|
|
570
|
+
});
|
|
508
571
|
export default app;
|
|
@@ -104,3 +104,62 @@ export async function steerSessionTask(taskId) {
|
|
|
104
104
|
const result = await bot.steerTask(taskId);
|
|
105
105
|
return { ok: true, steered: result.steered };
|
|
106
106
|
}
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
// Interaction prompt control (human-in-the-loop)
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
export function interactionSelectOption(promptId, optionValue, opts) {
|
|
111
|
+
const bot = runtime.getBotRef();
|
|
112
|
+
if (!bot)
|
|
113
|
+
return { ok: false, error: 'Bot is not running' };
|
|
114
|
+
const result = bot.interactionSelectOption(promptId, optionValue, opts);
|
|
115
|
+
if (!result)
|
|
116
|
+
return { ok: false, error: 'Prompt not found or no longer active' };
|
|
117
|
+
return { ok: true, completed: result.completed, advanced: result.advanced };
|
|
118
|
+
}
|
|
119
|
+
export function interactionSubmitText(promptId, text) {
|
|
120
|
+
const bot = runtime.getBotRef();
|
|
121
|
+
if (!bot)
|
|
122
|
+
return { ok: false, error: 'Bot is not running' };
|
|
123
|
+
const result = bot.interactionSubmitText(promptId, text);
|
|
124
|
+
if (!result)
|
|
125
|
+
return { ok: false, error: 'Prompt not found or not awaiting text' };
|
|
126
|
+
return { ok: true, completed: result.completed, advanced: result.advanced };
|
|
127
|
+
}
|
|
128
|
+
export function interactionSkip(promptId) {
|
|
129
|
+
const bot = runtime.getBotRef();
|
|
130
|
+
if (!bot)
|
|
131
|
+
return { ok: false, error: 'Bot is not running' };
|
|
132
|
+
const result = bot.interactionSkip(promptId);
|
|
133
|
+
if (!result)
|
|
134
|
+
return { ok: false, error: 'Prompt not found or no longer active' };
|
|
135
|
+
return { ok: true, completed: result.completed, advanced: result.advanced };
|
|
136
|
+
}
|
|
137
|
+
export function interactionCancel(promptId) {
|
|
138
|
+
const bot = runtime.getBotRef();
|
|
139
|
+
if (!bot)
|
|
140
|
+
return { ok: false, error: 'Bot is not running' };
|
|
141
|
+
const result = bot.interactionCancel(promptId);
|
|
142
|
+
if (!result)
|
|
143
|
+
return { ok: false, error: 'Prompt not found or no longer active' };
|
|
144
|
+
return { ok: true };
|
|
145
|
+
}
|
|
146
|
+
export function getInteractionPrompt(promptId) {
|
|
147
|
+
const bot = runtime.getBotRef();
|
|
148
|
+
if (!bot)
|
|
149
|
+
return { ok: false, error: 'Bot is not running' };
|
|
150
|
+
const prompt = bot.interactionPrompt(promptId);
|
|
151
|
+
if (!prompt)
|
|
152
|
+
return { ok: true, prompt: null };
|
|
153
|
+
return {
|
|
154
|
+
ok: true,
|
|
155
|
+
prompt: {
|
|
156
|
+
promptId: prompt.promptId,
|
|
157
|
+
taskId: prompt.taskId,
|
|
158
|
+
title: prompt.title,
|
|
159
|
+
hint: prompt.hint,
|
|
160
|
+
questions: prompt.questions,
|
|
161
|
+
currentIndex: prompt.currentIndex,
|
|
162
|
+
answers: prompt.answers,
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pikiclaw",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.18",
|
|
4
4
|
"description": "Put the world's smartest AI agents in your pocket. Command local Claude & Gemini via IM. | 让最好用的 IM 变成你电脑上的顶级 Agent 控制台",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"test:watch": "vitest"
|
|
38
38
|
},
|
|
39
39
|
"engines": {
|
|
40
|
-
"node": ">=
|
|
40
|
+
"node": ">=20.0.0"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@tailwindcss/vite": "^4.2.1",
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Maps Codex user-input requests into IM human-loop prompts.
|
|
3
|
-
*/
|
|
4
|
-
export function buildCodexHumanLoopPrompt(request) {
|
|
5
|
-
return {
|
|
6
|
-
title: 'User Input Required',
|
|
7
|
-
detail: 'codex',
|
|
8
|
-
hint: 'Use the buttons when available. Reply with text when prompted.',
|
|
9
|
-
questions: request.questions.map(question => ({
|
|
10
|
-
id: question.id,
|
|
11
|
-
header: question.header || 'Question',
|
|
12
|
-
prompt: question.question,
|
|
13
|
-
options: question.options?.map(option => ({
|
|
14
|
-
label: option.label,
|
|
15
|
-
description: option.description,
|
|
16
|
-
value: option.label,
|
|
17
|
-
})) || null,
|
|
18
|
-
allowFreeform: question.isOther || !question.options?.length,
|
|
19
|
-
secret: question.isSecret,
|
|
20
|
-
allowEmpty: true,
|
|
21
|
-
})),
|
|
22
|
-
resolveWith: answers => ({
|
|
23
|
-
answers: Object.fromEntries(Object.entries(answers).map(([id, values]) => [id, { answers: values }])),
|
|
24
|
-
}),
|
|
25
|
-
};
|
|
26
|
-
}
|