@promptbook/cli 0.114.0-12 → 0.114.0-13
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/apps/agents-server/src/app/agents/[agentName]/AgentChatWrapper.tsx +7 -4
- package/apps/agents-server/src/app/agents/[agentName]/chat/AgentGoalChatPlannedMessages.tsx +23 -8
- package/apps/agents-server/src/app/agents/[agentName]/chat/CanonicalAgentChatSurface.tsx +14 -3
- package/apps/agents-server/src/app/agents/[agentName]/chat/page.tsx +2 -20
- package/apps/agents-server/src/app/agents/[agentName]/page.tsx +2 -1
- package/apps/agents-server/src/app/agents/[agentName]/projectReferenceActions.ts +61 -0
- package/apps/agents-server/src/components/AgentProjects/resolveLatestCompletedAgentMessageKey.ts +32 -0
- package/apps/agents-server/src/components/AgentProjects/useAgentProjectMarkdownReferences.ts +124 -2
- package/apps/agents-server/src/languages/ServerTranslationKeys.ts +3 -0
- package/apps/agents-server/src/languages/translations/czech.yaml +3 -0
- package/apps/agents-server/src/languages/translations/english.yaml +3 -0
- package/apps/agents-server/src/tools/agentGoalChatTimeoutToolFunctions.ts +24 -2
- package/apps/agents-server/src/tools/agentGoalChatTimeoutTools.ts +5 -4
- package/apps/agents-server/src/utils/agentGoalChat/agentGoalChatPlannedMessageActions.ts +38 -9
- package/apps/agents-server/src/utils/agentGoalChat/createAgentGoalChatNoteContent.ts +15 -6
- package/apps/agents-server/src/utils/agentProjects/listAgentProjectNames.ts +35 -0
- package/apps/agents-server/src/utils/agentProjects/listAgentProjects.ts +6 -23
- package/apps/agents-server/src/utils/agentProjects/resolveAgentChatProjectReferences.ts +25 -0
- package/apps/agents-server/src/utils/chatMessageChips/createAnsweredMessageChipToolCalls.ts +37 -0
- package/apps/agents-server/src/utils/chatMessageChips/createPlannedMessageChipToolCalls.ts +39 -0
- package/apps/agents-server/src/utils/chatMessageChips/createTouchedProjectChipToolCalls.ts +75 -0
- package/apps/agents-server/src/utils/localChatRunner/applyLocalAgentPlannedMessageCommands.ts +212 -0
- package/apps/agents-server/src/utils/localChatRunner/prepareLocalAgentPlannedMessagesSidecar.ts +66 -0
- package/apps/agents-server/src/utils/localChatRunner/processLocalUserChatJob.ts +110 -22
- package/apps/agents-server/src/utils/userChat/persistUserChatJobTerminalState.ts +5 -0
- package/apps/agents-server/src/utils/userChat/userChatJobRunReport.ts +30 -0
- package/apps/agents-server/src/utils/userChatTimeout/createTimeoutWakeUpMessage.ts +9 -1
- package/apps/agents-server/src/utils/userChatTimeout/userChatTimeoutStore/repeatFiredUserChatTimeout.ts +74 -0
- package/apps/agents-server/src/utils/userChatTimeout/userChatTimeoutStore.ts +1 -0
- package/apps/agents-server/src/utils/userChatTimeout/userChatTimeoutWorker.ts +63 -54
- package/esm/index.es.js +639 -80
- package/esm/index.es.js.map +1 -1
- package/esm/scripts/run-agent-messages/messages/buildAgentGoalChatPromptSection.d.ts +22 -4
- package/esm/scripts/run-agent-messages/messages/buildAgentMessagePrompt.d.ts +3 -3
- package/esm/scripts/run-agent-messages/messages/loadAgentPlannedMessagesSidecar.d.ts +10 -0
- package/esm/scripts/run-agent-messages/messages/resolveTouchedAgentProjects.d.ts +15 -0
- package/esm/src/book-3.0/AgentMessageRunReport.d.ts +28 -0
- package/esm/src/book-3.0/AgentPlannedMessagesSidecar.d.ts +94 -0
- package/esm/src/book-3.0/AgentPlannedMessagesSidecar.test.d.ts +1 -0
- package/esm/src/book-3.0/createAgentMessageSidecarBaseName.d.ts +13 -0
- package/esm/src/book-components/Chat/utils/agentProjectToolCall.d.ts +46 -0
- package/esm/src/book-components/Chat/utils/timeoutToolCallPresentation.d.ts +24 -0
- package/esm/src/utils/agent-message-runtime/parseAgentMessageRuntimeLogEvents.d.ts +59 -0
- package/esm/src/utils/agent-message-runtime/resolveAgentMessageTouchedProjectNames.d.ts +19 -0
- package/esm/src/utils/agent-message-runtime/resolveAgentMessageTouchedProjectNames.test.d.ts +1 -0
- package/esm/src/version.d.ts +1 -1
- package/package.json +1 -1
- package/src/book-3.0/AgentMessageRunReport.ts +76 -0
- package/src/book-3.0/AgentPlannedMessagesSidecar.ts +229 -0
- package/src/book-3.0/AgentTeamConversationWorkspace.ts +3 -5
- package/src/book-3.0/createAgentMessageSidecarBaseName.ts +20 -0
- package/src/book-components/Chat/Chat/Chat.module.css +6 -1
- package/src/book-components/Chat/Chat/renderToolCallDetails.tsx +1 -12
- package/src/book-components/Chat/utils/agentProjectToolCall.ts +74 -0
- package/src/book-components/Chat/utils/getToolCallChipletInfo.ts +28 -0
- package/src/book-components/Chat/utils/timeoutToolCallPresentation.ts +23 -3
- package/src/other/templates/getTemplatesPipelineCollection.ts +784 -738
- package/src/utils/agent-message-runtime/parseAgentMessageRuntimeLogEvents.ts +105 -0
- package/src/utils/agent-message-runtime/resolveAgentMessageRuntimeActivity.ts +6 -54
- package/src/utils/agent-message-runtime/resolveAgentMessageTouchedProjectNames.ts +147 -0
- package/src/version.ts +2 -2
- package/src/versions.txt +1 -0
- package/umd/index.umd.js +639 -80
- package/umd/index.umd.js.map +1 -1
- package/umd/scripts/run-agent-messages/messages/buildAgentGoalChatPromptSection.d.ts +22 -4
- package/umd/scripts/run-agent-messages/messages/buildAgentMessagePrompt.d.ts +3 -3
- package/umd/scripts/run-agent-messages/messages/loadAgentPlannedMessagesSidecar.d.ts +10 -0
- package/umd/scripts/run-agent-messages/messages/resolveTouchedAgentProjects.d.ts +15 -0
- package/umd/src/book-3.0/AgentMessageRunReport.d.ts +28 -0
- package/umd/src/book-3.0/AgentPlannedMessagesSidecar.d.ts +94 -0
- package/umd/src/book-3.0/AgentPlannedMessagesSidecar.test.d.ts +1 -0
- package/umd/src/book-3.0/createAgentMessageSidecarBaseName.d.ts +13 -0
- package/umd/src/book-components/Chat/utils/agentProjectToolCall.d.ts +46 -0
- package/umd/src/book-components/Chat/utils/timeoutToolCallPresentation.d.ts +24 -0
- package/umd/src/utils/agent-message-runtime/parseAgentMessageRuntimeLogEvents.d.ts +59 -0
- package/umd/src/utils/agent-message-runtime/resolveAgentMessageTouchedProjectNames.d.ts +19 -0
- package/umd/src/utils/agent-message-runtime/resolveAgentMessageTouchedProjectNames.test.d.ts +1 -0
- package/umd/src/version.d.ts +1 -1
- package/esm/apps/agents-server/src/utils/agentGoalChat/agentGoalChatConstants.d.ts +0 -12
- package/umd/apps/agents-server/src/utils/agentGoalChat/agentGoalChatConstants.d.ts +0 -12
|
@@ -24,6 +24,14 @@ const MAX_LISTED_PLANNED_MESSAGES_LIMIT = 100;
|
|
|
24
24
|
*/
|
|
25
25
|
const ACTIVE_PLANNED_MESSAGE_STATUSES: ReadonlyArray<UserChatTimeoutStatus> = ['QUEUED', 'RUNNING'];
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Shortest repeat interval one agent may plan for itself.
|
|
29
|
+
*
|
|
30
|
+
* Planned messages repeat like `setInterval`, so an interval below one minute would turn the goal
|
|
31
|
+
* chat into an unattended self-invocation loop instead of a plan.
|
|
32
|
+
*/
|
|
33
|
+
const MINIMUM_PLANNED_MESSAGE_INTERVAL_MS = 60_000;
|
|
34
|
+
|
|
27
35
|
/**
|
|
28
36
|
* Minimal goal-chat identity needed when scheduling a planned message.
|
|
29
37
|
*/
|
|
@@ -41,6 +49,11 @@ export type AgentGoalChatPlannedMessageItem = {
|
|
|
41
49
|
readonly dueAt: string;
|
|
42
50
|
readonly isPaused: boolean;
|
|
43
51
|
readonly message: string | null;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Repeat interval in milliseconds, or `null` for a planned message that wakes the agent only once.
|
|
55
|
+
*/
|
|
56
|
+
readonly intervalMs: number | null;
|
|
44
57
|
};
|
|
45
58
|
|
|
46
59
|
/**
|
|
@@ -52,6 +65,11 @@ export type SetAgentGoalChatPlannedMessageResult = {
|
|
|
52
65
|
readonly timeoutId: string;
|
|
53
66
|
readonly dueAt: string;
|
|
54
67
|
readonly message: string | null;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Repeat interval in milliseconds of the scheduled planned message.
|
|
71
|
+
*/
|
|
72
|
+
readonly intervalMs: number | null;
|
|
55
73
|
};
|
|
56
74
|
|
|
57
75
|
/**
|
|
@@ -76,8 +94,14 @@ export type CancelAgentGoalChatPlannedMessageResult = {
|
|
|
76
94
|
|
|
77
95
|
/**
|
|
78
96
|
* Shared operations used by both model tools and the coding-agent runtime API.
|
|
97
|
+
*
|
|
98
|
+
* Planned messages behave like `setInterval`: `set` starts a repeating wake-up and only `cancel`
|
|
99
|
+
* stops it, so an agent that is happy with its plan does not have to do anything.
|
|
79
100
|
*/
|
|
80
101
|
export type AgentGoalChatPlannedMessageActions = {
|
|
102
|
+
/**
|
|
103
|
+
* Starts one repeating planned message, where `milliseconds` is its repeat interval.
|
|
104
|
+
*/
|
|
81
105
|
readonly set: (options: {
|
|
82
106
|
readonly agentPermanentId: string;
|
|
83
107
|
readonly milliseconds: unknown;
|
|
@@ -133,14 +157,16 @@ export function createAgentGoalChatPlannedMessageActions(
|
|
|
133
157
|
return {
|
|
134
158
|
async set(options): Promise<SetAgentGoalChatPlannedMessageResult> {
|
|
135
159
|
const agentPermanentId = parseRequiredText(options.agentPermanentId, 'agentPermanentId');
|
|
136
|
-
const
|
|
160
|
+
const intervalMs = parsePlannedMessageIntervalMs(options.milliseconds);
|
|
137
161
|
const message = parseRequiredText(options.message, 'message');
|
|
138
162
|
const goalChat = await dependencies.ensureAgentGoalChat(agentPermanentId);
|
|
163
|
+
// Note: A planned message repeats like `setInterval`, so the requested delay is also its interval
|
|
139
164
|
const plannedMessage = await dependencies.scheduleThreadScopedUserChatTimeout({
|
|
140
165
|
userId: goalChat.userId,
|
|
141
166
|
agentPermanentId,
|
|
142
167
|
chatId: goalChat.id,
|
|
143
|
-
durationMs:
|
|
168
|
+
durationMs: intervalMs,
|
|
169
|
+
recurrenceIntervalMs: intervalMs,
|
|
144
170
|
message,
|
|
145
171
|
});
|
|
146
172
|
|
|
@@ -150,6 +176,7 @@ export function createAgentGoalChatPlannedMessageActions(
|
|
|
150
176
|
timeoutId: plannedMessage.timeoutId,
|
|
151
177
|
dueAt: plannedMessage.dueAt,
|
|
152
178
|
message: plannedMessage.message,
|
|
179
|
+
intervalMs: plannedMessage.recurrenceIntervalMs,
|
|
153
180
|
};
|
|
154
181
|
},
|
|
155
182
|
|
|
@@ -209,20 +236,21 @@ export function createAgentGoalChatPlannedMessageActions(
|
|
|
209
236
|
export const AGENT_GOAL_CHAT_PLANNED_MESSAGE_ACTIONS = createAgentGoalChatPlannedMessageActions();
|
|
210
237
|
|
|
211
238
|
/**
|
|
212
|
-
* Parses one
|
|
239
|
+
* Parses one planned-message repeat interval.
|
|
213
240
|
*
|
|
214
|
-
* @param rawMilliseconds - Untrusted
|
|
215
|
-
* @returns
|
|
241
|
+
* @param rawMilliseconds - Untrusted interval supplied by a model or runtime API.
|
|
242
|
+
* @returns Repeat interval in whole milliseconds.
|
|
216
243
|
*/
|
|
217
|
-
function
|
|
244
|
+
function parsePlannedMessageIntervalMs(rawMilliseconds: unknown): number {
|
|
218
245
|
const milliseconds = Number(rawMilliseconds);
|
|
219
246
|
|
|
220
|
-
if (!Number.isFinite(milliseconds) || milliseconds <
|
|
247
|
+
if (!Number.isFinite(milliseconds) || milliseconds < MINIMUM_PLANNED_MESSAGE_INTERVAL_MS) {
|
|
221
248
|
throw new ParseError(
|
|
222
249
|
spaceTrim(`
|
|
223
|
-
Invalid planned-message
|
|
250
|
+
Invalid planned-message interval.
|
|
224
251
|
|
|
225
|
-
-
|
|
252
|
+
- A planned message **repeats** at this interval until it is cancelled.
|
|
253
|
+
- \`milliseconds\` must be a number of at least \`${MINIMUM_PLANNED_MESSAGE_INTERVAL_MS}\`.
|
|
226
254
|
`),
|
|
227
255
|
);
|
|
228
256
|
}
|
|
@@ -289,6 +317,7 @@ function createPlannedMessageListItem(plannedMessage: UserChatTimeoutRecord): Ag
|
|
|
289
317
|
dueAt: plannedMessage.dueAt,
|
|
290
318
|
isPaused: Boolean(plannedMessage.pausedAt),
|
|
291
319
|
message: plannedMessage.message,
|
|
320
|
+
intervalMs: plannedMessage.recurrenceIntervalMs,
|
|
292
321
|
};
|
|
293
322
|
}
|
|
294
323
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { spaceTrim } from 'spacetrim';
|
|
2
|
+
import { formatTimeoutDurationHuman } from '../../../../../src/book-components/Chat/utils/timeoutToolCallPresentation';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Lifecycle events of an agent that are recorded in its goal chat.
|
|
@@ -35,8 +36,8 @@ export function createAgentGoalChatLifecycleNoteContent(options: {
|
|
|
35
36
|
${block(goalSection)}
|
|
36
37
|
|
|
37
38
|
This is my own thread for acting towards that goal. I should work on it now when useful,
|
|
38
|
-
|
|
39
|
-
|
|
39
|
+
review the planned messages I already have, and plan a repeating wake-up with the
|
|
40
|
+
planned-message capability of my invocation when my goal needs one that is not planned yet.
|
|
40
41
|
`,
|
|
41
42
|
);
|
|
42
43
|
}
|
|
@@ -47,8 +48,10 @@ export function createAgentGoalChatLifecycleNoteContent(options: {
|
|
|
47
48
|
|
|
48
49
|
${block(goalSection)}
|
|
49
50
|
|
|
50
|
-
I should re-check my work and planned messages against this new goal
|
|
51
|
-
|
|
51
|
+
I should re-check my work and planned messages against this new goal and act on it now when useful.
|
|
52
|
+
My planned messages keep repeating on their own, so I keep the ones that still match this goal,
|
|
53
|
+
and cancel and re-plan only the ones that do not — announcing a wake-up without planning it
|
|
54
|
+
changes nothing.
|
|
52
55
|
`,
|
|
53
56
|
);
|
|
54
57
|
}
|
|
@@ -56,19 +59,25 @@ export function createAgentGoalChatLifecycleNoteContent(options: {
|
|
|
56
59
|
/**
|
|
57
60
|
* Builds the goal-chat note left when a planned message is scheduled.
|
|
58
61
|
*
|
|
59
|
-
* @param options - Planned-message identity, due time, and optional payload message.
|
|
62
|
+
* @param options - Planned-message identity, repeat interval, due time, and optional payload message.
|
|
60
63
|
* @returns Note content stored as one agent message.
|
|
61
64
|
*/
|
|
62
65
|
export function createAgentGoalChatPlannedMessageNoteContent(options: {
|
|
63
66
|
readonly timeoutId: string;
|
|
64
67
|
readonly dueAt: string;
|
|
68
|
+
readonly intervalMs?: number | null;
|
|
65
69
|
readonly message?: string | null;
|
|
66
70
|
}): string {
|
|
67
71
|
const plannedMessage = options.message?.trim() || '';
|
|
72
|
+
const headline = options.intervalMs
|
|
73
|
+
? `⏳ I planned a message that will wake me up every ${formatTimeoutDurationHuman(
|
|
74
|
+
options.intervalMs,
|
|
75
|
+
)}, for the first time at ${options.dueAt}.`
|
|
76
|
+
: `⏳ I planned a message that will wake me up at ${options.dueAt}.`;
|
|
68
77
|
|
|
69
78
|
return spaceTrim(
|
|
70
79
|
(block) => `
|
|
71
|
-
|
|
80
|
+
${headline}
|
|
72
81
|
|
|
73
82
|
${plannedMessage ? block(plannedMessage) : ''}
|
|
74
83
|
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { readdir } from 'fs/promises';
|
|
2
|
+
import { resolveAgentProjectsRootPath } from './agentProjectsPaths';
|
|
3
|
+
import { isMissingPathError } from './isMissingPathError';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Lists the directory names of all projects of one agent.
|
|
7
|
+
*
|
|
8
|
+
* This is the cheap half of `listAgentProjects` — it reads the `projects/` folder itself and never walks
|
|
9
|
+
* the project trees, so it can answer "did the set of projects change?" without measuring every project.
|
|
10
|
+
*
|
|
11
|
+
* A missing projects folder is reported as an empty list — the folder is created lazily
|
|
12
|
+
* when the agent (or the runner) first needs it.
|
|
13
|
+
*
|
|
14
|
+
* @param agentPermanentId - Permanent id of the agent.
|
|
15
|
+
* @returns Project directory names ordered by name.
|
|
16
|
+
*/
|
|
17
|
+
export async function listAgentProjectNames(agentPermanentId: string): Promise<ReadonlyArray<string>> {
|
|
18
|
+
const projectsRootPath = resolveAgentProjectsRootPath(agentPermanentId);
|
|
19
|
+
|
|
20
|
+
let projectsRootEntries;
|
|
21
|
+
try {
|
|
22
|
+
projectsRootEntries = await readdir(projectsRootPath, { withFileTypes: true });
|
|
23
|
+
} catch (error) {
|
|
24
|
+
if (isMissingPathError(error)) {
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
throw error;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return projectsRootEntries
|
|
32
|
+
.filter((projectsRootEntry) => projectsRootEntry.isDirectory())
|
|
33
|
+
.map((projectDirectoryEntry) => projectDirectoryEntry.name)
|
|
34
|
+
.sort((firstProjectName, secondProjectName) => firstProjectName.localeCompare(secondProjectName));
|
|
35
|
+
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { readdir } from 'fs/promises';
|
|
2
1
|
import type { AgentProjectInfo } from './AgentProjectInfo';
|
|
3
2
|
import { resolveAgentProjectsRootPath } from './agentProjectsPaths';
|
|
4
|
-
import {
|
|
3
|
+
import { listAgentProjectNames } from './listAgentProjectNames';
|
|
5
4
|
import { createAgentProjectInfo } from './resolveAgentProjectInfo';
|
|
6
5
|
|
|
7
6
|
/**
|
|
@@ -15,27 +14,11 @@ import { createAgentProjectInfo } from './resolveAgentProjectInfo';
|
|
|
15
14
|
*/
|
|
16
15
|
export async function listAgentProjects(agentPermanentId: string): Promise<ReadonlyArray<AgentProjectInfo>> {
|
|
17
16
|
const projectsRootPath = resolveAgentProjectsRootPath(agentPermanentId);
|
|
17
|
+
const projectNames = await listAgentProjectNames(agentPermanentId);
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
if (isMissingPathError(error)) {
|
|
24
|
-
return [];
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
throw error;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const projects = await Promise.all(
|
|
31
|
-
projectsRootEntries
|
|
32
|
-
.filter((projectsRootEntry) => projectsRootEntry.isDirectory())
|
|
33
|
-
.map((projectDirectoryEntry): Promise<AgentProjectInfo> =>
|
|
34
|
-
createAgentProjectInfo(projectsRootPath, projectDirectoryEntry.name),
|
|
35
|
-
),
|
|
36
|
-
);
|
|
37
|
-
|
|
38
|
-
return projects.sort((firstProject, secondProject) =>
|
|
39
|
-
firstProject.projectName.localeCompare(secondProject.projectName),
|
|
19
|
+
return await Promise.all(
|
|
20
|
+
projectNames.map(
|
|
21
|
+
(projectName): Promise<AgentProjectInfo> => createAgentProjectInfo(projectsRootPath, projectName),
|
|
22
|
+
),
|
|
40
23
|
);
|
|
41
24
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { resolveAgentProjectsAccess, type AgentProjectsAccessResolution } from './agentProjectAccess';
|
|
2
|
+
import type { AgentProjectReferenceInfo } from './AgentProjectReferenceInfo';
|
|
3
|
+
import { listAgentProjectChatReferences } from './listAgentProjectChatReferences';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Resolves the compact project references one chat surface may render as project chips.
|
|
7
|
+
*
|
|
8
|
+
* Every surface showing a chat — the agent profile, the chat page, and the action refreshing an open
|
|
9
|
+
* chat — gates the projects behind the same overview permission, so the decision lives here only.
|
|
10
|
+
*
|
|
11
|
+
* @param options - Owning agent and its already resolved project access, when the caller has it.
|
|
12
|
+
* @returns Display-only project references safe to send to the browser.
|
|
13
|
+
*/
|
|
14
|
+
export async function resolveAgentChatProjectReferences(options: {
|
|
15
|
+
readonly agentPermanentId: string;
|
|
16
|
+
readonly projectsAccess?: AgentProjectsAccessResolution;
|
|
17
|
+
}): Promise<ReadonlyArray<AgentProjectReferenceInfo>> {
|
|
18
|
+
const projectsAccess = options.projectsAccess || (await resolveAgentProjectsAccess(options.agentPermanentId));
|
|
19
|
+
|
|
20
|
+
if (!projectsAccess.isProjectOverviewVisible) {
|
|
21
|
+
return [];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return await listAgentProjectChatReferences(options.agentPermanentId);
|
|
25
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { ToolCall } from '@promptbook-local/types';
|
|
2
|
+
import type { AppliedAgentPlannedMessageCommand } from '../localChatRunner/applyLocalAgentPlannedMessageCommands';
|
|
3
|
+
import { createPlannedMessageChipToolCalls } from './createPlannedMessageChipToolCalls';
|
|
4
|
+
import { createTouchedProjectChipToolCalls } from './createTouchedProjectChipToolCalls';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Creates every extra chip shown below one answered chat message.
|
|
8
|
+
*
|
|
9
|
+
* This is the single place deciding what an answer shows besides its text: the wake-ups it
|
|
10
|
+
* planned and the projects it worked with. The chips travel as tool calls of the assistant
|
|
11
|
+
* message, so they are visible in every chat view — including external chats such as email,
|
|
12
|
+
* whose outbound reply is rendered from the message text alone and therefore stays unchanged.
|
|
13
|
+
*
|
|
14
|
+
* @param options - Owning agent plus what the answered message planned and touched.
|
|
15
|
+
* @returns Tool calls rendered as chips below the answer, in a stable order.
|
|
16
|
+
*/
|
|
17
|
+
export async function createAnsweredMessageChipToolCalls(options: {
|
|
18
|
+
readonly agentPermanentId: string;
|
|
19
|
+
readonly appliedPlannedMessageCommands: ReadonlyArray<AppliedAgentPlannedMessageCommand>;
|
|
20
|
+
readonly touchedProjectNames: ReadonlyArray<string>;
|
|
21
|
+
readonly createdAt?: NonNullable<ToolCall['createdAt']>;
|
|
22
|
+
}): Promise<ReadonlyArray<ToolCall>> {
|
|
23
|
+
const createdAt = options.createdAt || (new Date().toISOString() as NonNullable<ToolCall['createdAt']>);
|
|
24
|
+
const projectChipToolCalls = await createTouchedProjectChipToolCalls({
|
|
25
|
+
agentPermanentId: options.agentPermanentId,
|
|
26
|
+
touchedProjectNames: options.touchedProjectNames,
|
|
27
|
+
createdAt,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
return [
|
|
31
|
+
...projectChipToolCalls,
|
|
32
|
+
...createPlannedMessageChipToolCalls({
|
|
33
|
+
appliedCommands: options.appliedPlannedMessageCommands,
|
|
34
|
+
createdAt,
|
|
35
|
+
}),
|
|
36
|
+
];
|
|
37
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { ToolCall } from '@promptbook-local/types';
|
|
2
|
+
import {
|
|
3
|
+
CANCEL_TIMEOUT_TOOL_CALL_NAME,
|
|
4
|
+
SET_TIMEOUT_TOOL_CALL_NAME,
|
|
5
|
+
} from '../../../../../src/book-components/Chat/utils/timeoutToolCallPresentation';
|
|
6
|
+
import type { AppliedAgentPlannedMessageCommand } from '../localChatRunner/applyLocalAgentPlannedMessageCommands';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Prefix of the idempotency keys identifying planned-message chips.
|
|
10
|
+
*/
|
|
11
|
+
const PLANNED_MESSAGE_CHIP_IDEMPOTENCY_PREFIX = 'planned-message';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Creates the timeout chips of the message that planned or cancelled a wake-up.
|
|
15
|
+
*
|
|
16
|
+
* The chat already renders `set_timeout` and `cancel_timeout` tool calls as timeout chips with a
|
|
17
|
+
* detail popup, so one planned message becomes exactly one such tool call instead of a second
|
|
18
|
+
* timeout presentation.
|
|
19
|
+
*
|
|
20
|
+
* @param options - Planned-message commands applied for one answered message.
|
|
21
|
+
* @returns Timeout tool calls shown as chips below the answer.
|
|
22
|
+
*/
|
|
23
|
+
export function createPlannedMessageChipToolCalls(options: {
|
|
24
|
+
readonly appliedCommands: ReadonlyArray<AppliedAgentPlannedMessageCommand>;
|
|
25
|
+
readonly createdAt: NonNullable<ToolCall['createdAt']>;
|
|
26
|
+
}): ReadonlyArray<ToolCall> {
|
|
27
|
+
return options.appliedCommands.map(({ command, result }) => ({
|
|
28
|
+
name: result.action === 'set' ? SET_TIMEOUT_TOOL_CALL_NAME : CANCEL_TIMEOUT_TOOL_CALL_NAME,
|
|
29
|
+
arguments: {
|
|
30
|
+
...(command.milliseconds === undefined ? {} : { milliseconds: command.milliseconds }),
|
|
31
|
+
...(command.message === undefined ? {} : { message: command.message }),
|
|
32
|
+
...(command.timeoutId === undefined ? {} : { timeoutId: command.timeoutId }),
|
|
33
|
+
},
|
|
34
|
+
result,
|
|
35
|
+
state: 'COMPLETE',
|
|
36
|
+
idempotencyKey: `${PLANNED_MESSAGE_CHIP_IDEMPOTENCY_PREFIX}-${result.action}-${result.timeoutId}`,
|
|
37
|
+
createdAt: options.createdAt,
|
|
38
|
+
}));
|
|
39
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { ToolCall } from '@promptbook-local/types';
|
|
2
|
+
import { AGENT_PROJECT_TOOL_CALL_NAME } from '../../../../../src/book-components/Chat/utils/agentProjectToolCall';
|
|
3
|
+
import { buildAgentProjectProfileHref } from '../agentProjects/agentProjectHrefs';
|
|
4
|
+
import { isSafeAgentProjectPathSegment } from '../agentProjects/agentProjectsPaths';
|
|
5
|
+
import { resolveAgentProjectInfo } from '../agentProjects/resolveAgentProjectInfo';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Prefix of the idempotency keys identifying touched-project chips.
|
|
9
|
+
*/
|
|
10
|
+
const TOUCHED_PROJECT_CHIP_IDEMPOTENCY_PREFIX = 'agent-project';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Creates the project chips of one answered message.
|
|
14
|
+
*
|
|
15
|
+
* A project is reported by the runner once the answering harness viewed or edited any of its
|
|
16
|
+
* files, and it is resolved here so the chip carries the same human-readable project name and
|
|
17
|
+
* link as every other project surface. Projects that no longer exist are skipped.
|
|
18
|
+
*
|
|
19
|
+
* @param options - Owning agent and the project directory names its answer touched.
|
|
20
|
+
* @returns Project tool calls shown as chips below the answer.
|
|
21
|
+
*/
|
|
22
|
+
export async function createTouchedProjectChipToolCalls(options: {
|
|
23
|
+
readonly agentPermanentId: string;
|
|
24
|
+
readonly touchedProjectNames: ReadonlyArray<string>;
|
|
25
|
+
readonly createdAt: NonNullable<ToolCall['createdAt']>;
|
|
26
|
+
}): Promise<ReadonlyArray<ToolCall>> {
|
|
27
|
+
const projectChipToolCalls = await Promise.all(
|
|
28
|
+
options.touchedProjectNames
|
|
29
|
+
.filter((touchedProjectName) => isSafeAgentProjectPathSegment(touchedProjectName))
|
|
30
|
+
.map((touchedProjectName) =>
|
|
31
|
+
createTouchedProjectChipToolCall({
|
|
32
|
+
agentPermanentId: options.agentPermanentId,
|
|
33
|
+
projectName: touchedProjectName,
|
|
34
|
+
createdAt: options.createdAt,
|
|
35
|
+
}),
|
|
36
|
+
),
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
return projectChipToolCalls.filter((projectChipToolCall): projectChipToolCall is ToolCall =>
|
|
40
|
+
Boolean(projectChipToolCall),
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Creates the chip of one touched project.
|
|
46
|
+
*
|
|
47
|
+
* @param options - Owning agent and one touched project directory name.
|
|
48
|
+
* @returns Project tool call, or `null` when the project no longer exists.
|
|
49
|
+
*
|
|
50
|
+
* @private function of `createTouchedProjectChipToolCalls`
|
|
51
|
+
*/
|
|
52
|
+
async function createTouchedProjectChipToolCall(options: {
|
|
53
|
+
readonly agentPermanentId: string;
|
|
54
|
+
readonly projectName: string;
|
|
55
|
+
readonly createdAt: NonNullable<ToolCall['createdAt']>;
|
|
56
|
+
}): Promise<ToolCall | null> {
|
|
57
|
+
const project = await resolveAgentProjectInfo(options.agentPermanentId, options.projectName);
|
|
58
|
+
|
|
59
|
+
if (!project) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
name: AGENT_PROJECT_TOOL_CALL_NAME,
|
|
65
|
+
arguments: { projectName: project.projectName },
|
|
66
|
+
result: {
|
|
67
|
+
projectName: project.projectName,
|
|
68
|
+
displayName: project.displayName,
|
|
69
|
+
projectHref: buildAgentProjectProfileHref(options.agentPermanentId, project.projectName),
|
|
70
|
+
},
|
|
71
|
+
state: 'COMPLETE',
|
|
72
|
+
idempotencyKey: `${TOUCHED_PROJECT_CHIP_IDEMPOTENCY_PREFIX}-${project.projectName.toLowerCase()}`,
|
|
73
|
+
createdAt: options.createdAt,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { readFile, rm } from 'fs/promises';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { serializeError } from '@promptbook-local/utils';
|
|
4
|
+
import {
|
|
5
|
+
createAgentPlannedMessagesSidecarPath,
|
|
6
|
+
parseAgentPlannedMessagesSidecar,
|
|
7
|
+
type AgentPlannedMessageCommand,
|
|
8
|
+
} from '../../../../../src/book-3.0/AgentPlannedMessagesSidecar';
|
|
9
|
+
import {
|
|
10
|
+
AGENT_GOAL_CHAT_PLANNED_MESSAGE_ACTIONS,
|
|
11
|
+
type CancelAgentGoalChatPlannedMessageResult,
|
|
12
|
+
type SetAgentGoalChatPlannedMessageResult,
|
|
13
|
+
} from '../agentGoalChat/agentGoalChatPlannedMessageActions';
|
|
14
|
+
import type { UserChatJobRecord } from '../userChat/UserChatJobRecord';
|
|
15
|
+
import type { LocalUserChatJobMetadata } from './LocalUserChatJobMetadata';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Planned-message operations used while applying one sidecar.
|
|
19
|
+
*/
|
|
20
|
+
export type LocalAgentPlannedMessageActions = Pick<typeof AGENT_GOAL_CHAT_PLANNED_MESSAGE_ACTIONS, 'set' | 'cancel'>;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* One planned-message command that was applied together with its result.
|
|
24
|
+
*
|
|
25
|
+
* The command carries what the agent asked for (for example the requested delay) and the result
|
|
26
|
+
* carries what really happened, so the chat can show the wake-up below the message that planned it.
|
|
27
|
+
*/
|
|
28
|
+
export type AppliedAgentPlannedMessageCommand = {
|
|
29
|
+
readonly command: AgentPlannedMessageCommand;
|
|
30
|
+
readonly result: SetAgentGoalChatPlannedMessageResult | CancelAgentGoalChatPlannedMessageResult;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Applies every planned-message command a coding harness wrote while answering one message.
|
|
35
|
+
*
|
|
36
|
+
* The sidecar is consumed exactly once: it is removed afterwards so a replayed worker tick can never
|
|
37
|
+
* schedule the very same wake-up twice.
|
|
38
|
+
*
|
|
39
|
+
* @param options - Answered job, its agent folder path, and the local runner metadata of the message.
|
|
40
|
+
* @returns Planned-message commands that were applied successfully, in sidecar order.
|
|
41
|
+
*/
|
|
42
|
+
export async function applyLocalAgentPlannedMessageCommands(options: {
|
|
43
|
+
readonly job: Pick<UserChatJobRecord, 'id' | 'chatId' | 'agentPermanentId'>;
|
|
44
|
+
readonly agentDirectoryPath: string;
|
|
45
|
+
readonly metadata: LocalUserChatJobMetadata;
|
|
46
|
+
readonly actions?: LocalAgentPlannedMessageActions;
|
|
47
|
+
}): Promise<ReadonlyArray<AppliedAgentPlannedMessageCommand>> {
|
|
48
|
+
const sidecarPath = resolveLocalAgentPlannedMessagesSidecarPath(options.agentDirectoryPath, options.metadata);
|
|
49
|
+
const sidecarContent = await readOptionalTextFile(sidecarPath);
|
|
50
|
+
|
|
51
|
+
await removeLocalAgentPlannedMessagesSidecar(options.agentDirectoryPath, options.metadata);
|
|
52
|
+
|
|
53
|
+
if (sidecarContent === null) {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const sidecar = parseAgentPlannedMessagesSidecar(sidecarContent);
|
|
58
|
+
|
|
59
|
+
if (!sidecar) {
|
|
60
|
+
console.warn('[local-chat-runner] planned_messages_sidecar_unreadable', {
|
|
61
|
+
chatId: options.job.chatId,
|
|
62
|
+
jobId: options.job.id,
|
|
63
|
+
});
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const actions = options.actions || AGENT_GOAL_CHAT_PLANNED_MESSAGE_ACTIONS;
|
|
68
|
+
const appliedCommands: Array<AppliedAgentPlannedMessageCommand> = [];
|
|
69
|
+
|
|
70
|
+
for (const command of sidecar.commands) {
|
|
71
|
+
const result = await applyLocalAgentPlannedMessageCommand({
|
|
72
|
+
command,
|
|
73
|
+
actions,
|
|
74
|
+
agentPermanentId: options.job.agentPermanentId,
|
|
75
|
+
job: options.job,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
if (result) {
|
|
79
|
+
appliedCommands.push({ command, result });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return appliedCommands;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Removes the planned-message sidecar of one queued message.
|
|
88
|
+
*
|
|
89
|
+
* @param agentDirectoryPath - Absolute path of the local agent folder.
|
|
90
|
+
* @param metadata - Local runner metadata of the queued message.
|
|
91
|
+
*/
|
|
92
|
+
export async function removeLocalAgentPlannedMessagesSidecar(
|
|
93
|
+
agentDirectoryPath: string,
|
|
94
|
+
metadata: LocalUserChatJobMetadata,
|
|
95
|
+
): Promise<void> {
|
|
96
|
+
await rm(resolveLocalAgentPlannedMessagesSidecarPath(agentDirectoryPath, metadata), { force: true }).catch(
|
|
97
|
+
() => undefined,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Dispatches one planned-message command through the shared planned-message actions.
|
|
103
|
+
*
|
|
104
|
+
* One rejected command never blocks the remaining ones, because the agent already answered and the
|
|
105
|
+
* harness owns this sidecar.
|
|
106
|
+
*
|
|
107
|
+
* @param options - Command, actions, and the agent whose goal chat owns the planned message.
|
|
108
|
+
* @returns Result of the applied command, or `null` when it was rejected.
|
|
109
|
+
*
|
|
110
|
+
* @private function of `applyLocalAgentPlannedMessageCommands`
|
|
111
|
+
*/
|
|
112
|
+
async function applyLocalAgentPlannedMessageCommand(options: {
|
|
113
|
+
readonly command: AgentPlannedMessageCommand;
|
|
114
|
+
readonly actions: LocalAgentPlannedMessageActions;
|
|
115
|
+
readonly agentPermanentId: string;
|
|
116
|
+
readonly job: Pick<UserChatJobRecord, 'id' | 'chatId'>;
|
|
117
|
+
}): Promise<AppliedAgentPlannedMessageCommand['result'] | null> {
|
|
118
|
+
try {
|
|
119
|
+
if (options.command.action === 'set') {
|
|
120
|
+
const plannedMessage = await options.actions.set({
|
|
121
|
+
agentPermanentId: options.agentPermanentId,
|
|
122
|
+
milliseconds: options.command.milliseconds,
|
|
123
|
+
message: options.command.message,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
console.info('[local-chat-runner]', 'planned_message_set', {
|
|
127
|
+
chatId: options.job.chatId,
|
|
128
|
+
jobId: options.job.id,
|
|
129
|
+
timeoutId: plannedMessage.timeoutId,
|
|
130
|
+
dueAt: plannedMessage.dueAt,
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
return plannedMessage;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const cancelledPlannedMessage = await options.actions.cancel({
|
|
137
|
+
agentPermanentId: options.agentPermanentId,
|
|
138
|
+
timeoutId: options.command.timeoutId,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
console.info('[local-chat-runner]', 'planned_message_cancel', {
|
|
142
|
+
chatId: options.job.chatId,
|
|
143
|
+
jobId: options.job.id,
|
|
144
|
+
timeoutId: cancelledPlannedMessage.timeoutId,
|
|
145
|
+
status: cancelledPlannedMessage.status,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
return cancelledPlannedMessage.status === 'cancelled' ? cancelledPlannedMessage : null;
|
|
149
|
+
} catch (error) {
|
|
150
|
+
console.error('[local-chat-runner]', 'planned_message_command_failed', {
|
|
151
|
+
chatId: options.job.chatId,
|
|
152
|
+
jobId: options.job.id,
|
|
153
|
+
action: options.command.action,
|
|
154
|
+
error: serializeError(error as Error),
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Resolves the absolute planned-message sidecar path of one queued message.
|
|
163
|
+
*
|
|
164
|
+
* @param agentDirectoryPath - Absolute path of the local agent folder.
|
|
165
|
+
* @param metadata - Local runner metadata of the queued message.
|
|
166
|
+
* @returns Absolute sidecar path.
|
|
167
|
+
*
|
|
168
|
+
* @private function of `applyLocalAgentPlannedMessageCommands`
|
|
169
|
+
*/
|
|
170
|
+
function resolveLocalAgentPlannedMessagesSidecarPath(
|
|
171
|
+
agentDirectoryPath: string,
|
|
172
|
+
metadata: LocalUserChatJobMetadata,
|
|
173
|
+
): string {
|
|
174
|
+
return join(agentDirectoryPath, createAgentPlannedMessagesSidecarPath(metadata.fileName));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Reads one text file and treats a missing planned-message sidecar as absent.
|
|
179
|
+
*
|
|
180
|
+
* @param path - Absolute sidecar path.
|
|
181
|
+
* @returns File content, or `null` when the sidecar does not exist.
|
|
182
|
+
*
|
|
183
|
+
* @private function of `applyLocalAgentPlannedMessageCommands`
|
|
184
|
+
*/
|
|
185
|
+
async function readOptionalTextFile(path: string): Promise<string | null> {
|
|
186
|
+
try {
|
|
187
|
+
return await readFile(path, 'utf-8');
|
|
188
|
+
} catch (error) {
|
|
189
|
+
if (isFileNotFoundError(error)) {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
throw error;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Returns true when one filesystem error indicates a missing path.
|
|
199
|
+
*
|
|
200
|
+
* @param error - Caught filesystem failure.
|
|
201
|
+
* @returns `true` for missing-path errors.
|
|
202
|
+
*
|
|
203
|
+
* @private function of `applyLocalAgentPlannedMessageCommands`
|
|
204
|
+
*/
|
|
205
|
+
function isFileNotFoundError(error: unknown): boolean {
|
|
206
|
+
return Boolean(
|
|
207
|
+
error &&
|
|
208
|
+
typeof error === 'object' &&
|
|
209
|
+
'code' in error &&
|
|
210
|
+
((error as { code?: string }).code === 'ENOENT' || (error as { code?: string }).code === 'ENOTDIR'),
|
|
211
|
+
);
|
|
212
|
+
}
|