@xfey/tutti 0.1.60 → 0.1.61
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/collaboration-state/clarification-records.d.ts +0 -9
- package/dist/collaboration-state/clarification-records.js +0 -57
- package/dist/collaboration-state/index.d.ts +6 -5
- package/dist/collaboration-state/index.js +5 -4
- package/dist/collaboration-state/messages.d.ts +4 -6
- package/dist/collaboration-state/messages.js +22 -29
- package/dist/collaboration-state/scratchpad-source-state.d.ts +4 -12
- package/dist/collaboration-state/scratchpad-source-state.js +24 -58
- package/dist/collaboration-state/scratchpad.d.ts +1 -2
- package/dist/collaboration-state/scratchpad.js +0 -59
- package/dist/collaboration-state/storage-types.d.ts +25 -5
- package/dist/collaboration-state/task-compile-context.d.ts +40 -0
- package/dist/collaboration-state/task-compile-context.js +273 -0
- package/dist/collaboration-state/types.d.ts +39 -9
- package/dist/control-plane/clarification-commands.d.ts +0 -1
- package/dist/control-plane/clarification-commands.js +0 -1
- package/dist/control-plane/index.js +16 -3
- package/dist/control-plane/project-context-bootstrap.d.ts +4 -4
- package/dist/control-plane/project-context-bootstrap.js +7 -12
- package/dist/control-plane/scratchpad-auto-refresh.js +12 -1
- package/dist/control-plane/scratchpad-refresh-start.js +77 -94
- package/dist/control-plane/scratchpad-refresh.js +3 -1
- package/dist/control-plane/scratchpad-source-messages.d.ts +15 -7
- package/dist/control-plane/scratchpad-source-messages.js +53 -69
- package/dist/control-plane/task-compile-clarification-context.d.ts +9 -0
- package/dist/control-plane/task-compile-clarification-context.js +70 -0
- package/dist/control-plane/task-compile-continuation.js +28 -56
- package/dist/control-plane/task-compile-output.js +50 -14
- package/dist/control-plane/task-compile-start.js +119 -55
- package/dist/control-plane/workflows/openai.d.ts +2 -1
- package/dist/control-plane/workflows/openai.js +13 -27
- package/dist/control-plane/workflows/types.d.ts +27 -17
- package/migrations/0014_scratchpad_task_compile_context.sql +147 -0
- package/migrations/README.md +2 -1
- package/package.json +1 -1
- package/prompts/procedures/README.md +3 -3
- package/prompts/procedures/scratchpad-refresh.md +17 -9
- package/prompts/procedures/task-compile.md +6 -3
- package/prompts/prompt-flow-map.md +21 -14
|
@@ -1,16 +1,11 @@
|
|
|
1
1
|
import { createWorkflowInvocationRef, } from "@tutti/shared/ids";
|
|
2
|
-
import {
|
|
2
|
+
import { readActiveTaskCompileContext, readMainChatMessagesForScratchpadCycle, readScratchpadSourceState, readWorklistProjection, markScratchpadRefreshFailed, markScratchpadRefreshStarted, } from "../collaboration-state/index.js";
|
|
3
3
|
import { readProjectBriefProjection, readProjectBriefSourceDocuments, } from "../project-brief/index.js";
|
|
4
4
|
import { logProcedureExecutionError } from "./procedure-logging.js";
|
|
5
5
|
import { applyScratchpadRefreshOutput } from "./scratchpad-refresh.js";
|
|
6
|
-
import {
|
|
7
|
-
const DEFAULT_SCRATCHPAD_SOURCE_MESSAGE_LIMIT = 50;
|
|
8
|
-
const INCREMENTAL_SCRATCHPAD_SOURCE_MESSAGE_LIMIT = 200;
|
|
9
|
-
const RECENT_CONTEXT_RAW_MESSAGE_LIMIT = 12;
|
|
10
|
-
const RECENT_MESSAGE_WINDOW_SIZE = 5;
|
|
11
|
-
const DEFAULT_CLARIFICATION_SOURCE_ROUND_LIMIT = 20;
|
|
12
|
-
const CLARIFICATION_SOURCE_MESSAGE_LIMIT = 50;
|
|
6
|
+
import { compactScratchpadSourceMessages, compactScratchpadWorklist, filterScratchpadSourceMessages, MAX_SOURCE_MESSAGES_TOTAL_CHARS, MIN_SOURCE_MESSAGE_TEXT_CHARS, ScratchpadSourceBudgetExceededError, } from "./scratchpad-source-messages.js";
|
|
13
7
|
const SCRATCHPAD_REFRESH_FAILURE_BACKOFF_MS = 60_000;
|
|
8
|
+
const MAX_SCRATCHPAD_WORKFLOW_INPUT_CHARS = 30_000;
|
|
14
9
|
function readScratchpadProjectBrief(input) {
|
|
15
10
|
if (input.projectContext === undefined) {
|
|
16
11
|
return undefined;
|
|
@@ -35,105 +30,93 @@ function readScratchpadProjectBrief(input) {
|
|
|
35
30
|
return undefined;
|
|
36
31
|
}
|
|
37
32
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
?
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const newMainChatSourceMessages = isInitialMainChatRefresh ? [] : mainChatSourceMessages;
|
|
59
|
-
const recentMessageLimit = Math.max(0, RECENT_MESSAGE_WINDOW_SIZE - newMainChatSourceMessages.length);
|
|
60
|
-
const firstNewMainChatSource = newMainChatSourceMessages[0];
|
|
61
|
-
const recentMessages = recentMessageLimit === 0
|
|
62
|
-
? []
|
|
63
|
-
: isInitialMainChatRefresh
|
|
64
|
-
? mainChatSourceMessages.slice(-recentMessageLimit)
|
|
65
|
-
: firstNewMainChatSource === undefined
|
|
66
|
-
? filterScratchpadSourceMessages(readMainChatMessageWindow(store.db, {
|
|
67
|
-
limit: RECENT_CONTEXT_RAW_MESSAGE_LIMIT,
|
|
68
|
-
}).items).slice(-recentMessageLimit)
|
|
69
|
-
: filterScratchpadSourceMessages(readMainChatMessagesBeforeCursor(store.db, {
|
|
70
|
-
before_created_at: firstNewMainChatSource.created_at,
|
|
71
|
-
before_id: firstNewMainChatSource.id,
|
|
72
|
-
limit: RECENT_CONTEXT_RAW_MESSAGE_LIMIT,
|
|
73
|
-
})).slice(-recentMessageLimit);
|
|
74
|
-
const clarificationRounds = sourceState.task_compile_cursor === undefined
|
|
75
|
-
? readSubmittedTaskCompileClarificationRounds(store.db, sourceFallbackSince === undefined
|
|
76
|
-
? {
|
|
77
|
-
limit: DEFAULT_CLARIFICATION_SOURCE_ROUND_LIMIT,
|
|
78
|
-
}
|
|
79
|
-
: {
|
|
80
|
-
afterSubmittedAt: sourceFallbackSince,
|
|
81
|
-
limit: DEFAULT_CLARIFICATION_SOURCE_ROUND_LIMIT,
|
|
82
|
-
})
|
|
83
|
-
: readSubmittedTaskCompileClarificationRoundsAfterCursor(store.db, {
|
|
84
|
-
afterSubmittedAt: sourceState.task_compile_cursor.submitted_at,
|
|
85
|
-
afterRoundId: sourceState.task_compile_cursor.round_id,
|
|
86
|
-
limit: DEFAULT_CLARIFICATION_SOURCE_ROUND_LIMIT,
|
|
87
|
-
});
|
|
88
|
-
const clarificationSourceMessages = clarificationRounds.flatMap((round) => {
|
|
89
|
-
const window = readClarificationRoundMessagesWindow(store.db, round.id, {
|
|
90
|
-
anchor: "start",
|
|
91
|
-
limit: CLARIFICATION_SOURCE_MESSAGE_LIMIT,
|
|
92
|
-
});
|
|
93
|
-
return filterClarificationRoundScratchpadSourceMessages(window?.items ?? []);
|
|
94
|
-
});
|
|
95
|
-
const sourceMessages = sortScratchpadSourceMessages([
|
|
96
|
-
...newMainChatSourceMessages,
|
|
97
|
-
...clarificationSourceMessages,
|
|
98
|
-
]);
|
|
99
|
-
const sourceRefMessages = sortScratchpadSourceMessages([
|
|
100
|
-
...(isInitialMainChatRefresh ? recentMessages : []),
|
|
101
|
-
...sourceMessages,
|
|
102
|
-
]);
|
|
103
|
-
const latestMainChatSource = mainChatSourceMessages.at(-1);
|
|
104
|
-
const latestClarificationRound = clarificationRounds.at(-1);
|
|
105
|
-
const cursorUpdate = {};
|
|
106
|
-
if (latestMainChatSource !== undefined) {
|
|
107
|
-
cursorUpdate.main_chat_cursor = {
|
|
108
|
-
created_at: latestMainChatSource.created_at,
|
|
109
|
-
message_id: latestMainChatSource.id,
|
|
33
|
+
function workflowInputSize(input) {
|
|
34
|
+
return JSON.stringify(input).length;
|
|
35
|
+
}
|
|
36
|
+
function fitScratchpadRefreshWorkflowInput(input) {
|
|
37
|
+
const base = {
|
|
38
|
+
...(input.projectBrief === undefined ? {} : { project_brief: input.projectBrief }),
|
|
39
|
+
...(input.activeTaskCompile === undefined
|
|
40
|
+
? {}
|
|
41
|
+
: { active_task_compile: input.activeTaskCompile }),
|
|
42
|
+
};
|
|
43
|
+
const worklistVariants = [
|
|
44
|
+
compactScratchpadWorklist(input.worklist),
|
|
45
|
+
compactScratchpadWorklist(input.worklist, { summaryChars: 320 }),
|
|
46
|
+
compactScratchpadWorklist(input.worklist, { includeSummaries: false }),
|
|
47
|
+
];
|
|
48
|
+
for (const worklist of worklistVariants) {
|
|
49
|
+
const candidate = {
|
|
50
|
+
...base,
|
|
51
|
+
worklist,
|
|
52
|
+
messages: compactScratchpadSourceMessages(input.sourceMessages),
|
|
110
53
|
};
|
|
54
|
+
if (workflowInputSize(candidate) <= MAX_SCRATCHPAD_WORKFLOW_INPUT_CHARS) {
|
|
55
|
+
return candidate;
|
|
56
|
+
}
|
|
111
57
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
58
|
+
const worklist = worklistVariants.at(-1);
|
|
59
|
+
if (worklist === undefined) {
|
|
60
|
+
throw new ScratchpadSourceBudgetExceededError();
|
|
61
|
+
}
|
|
62
|
+
const minimumMessageBudget = input.sourceMessages.length * MIN_SOURCE_MESSAGE_TEXT_CHARS;
|
|
63
|
+
for (let messageBudget = MAX_SOURCE_MESSAGES_TOTAL_CHARS - 1_000; messageBudget >= minimumMessageBudget; messageBudget -= 1_000) {
|
|
64
|
+
const candidate = {
|
|
65
|
+
...base,
|
|
66
|
+
worklist,
|
|
67
|
+
messages: compactScratchpadSourceMessages(input.sourceMessages, {
|
|
68
|
+
totalTextChars: messageBudget,
|
|
69
|
+
}),
|
|
116
70
|
};
|
|
71
|
+
if (workflowInputSize(candidate) <= MAX_SCRATCHPAD_WORKFLOW_INPUT_CHARS) {
|
|
72
|
+
return candidate;
|
|
73
|
+
}
|
|
117
74
|
}
|
|
75
|
+
throw new ScratchpadSourceBudgetExceededError();
|
|
76
|
+
}
|
|
77
|
+
export function readControlPlaneScratchpadSourceBatch(store, options = {}) {
|
|
78
|
+
const sourceState = readScratchpadSourceState(store.db);
|
|
79
|
+
const mainChatMessages = readMainChatMessagesForScratchpadCycle(store.db, sourceState.cycle_start_cursor);
|
|
80
|
+
const sourceMessages = filterScratchpadSourceMessages(mainChatMessages);
|
|
81
|
+
const latestSource = sourceMessages.at(-1);
|
|
82
|
+
const unrefreshedSourceCount = sourceMessages.filter((message) => {
|
|
83
|
+
const refreshedThrough = sourceState.refreshed_through_cursor;
|
|
84
|
+
if (refreshedThrough === undefined) {
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
const timeOrder = message.created_at.localeCompare(refreshedThrough.created_at);
|
|
88
|
+
return timeOrder > 0 || (timeOrder === 0 && message.id > refreshedThrough.message_id);
|
|
89
|
+
}).length;
|
|
90
|
+
const activeContext = readActiveTaskCompileContext(store.db);
|
|
118
91
|
const projectBrief = readScratchpadProjectBrief({
|
|
119
92
|
store,
|
|
120
93
|
...(options.projectContext === undefined ? {} : { projectContext: options.projectContext }),
|
|
121
94
|
...(options.now === undefined ? {} : { now: options.now }),
|
|
122
95
|
});
|
|
96
|
+
const workflowInput = fitScratchpadRefreshWorkflowInput({
|
|
97
|
+
...(projectBrief === undefined ? {} : { projectBrief }),
|
|
98
|
+
...(activeContext === null
|
|
99
|
+
? {}
|
|
100
|
+
: { activeTaskCompile: activeContext.scratchpad_snapshot }),
|
|
101
|
+
worklist: readWorklistProjection(store.db),
|
|
102
|
+
sourceMessages,
|
|
103
|
+
});
|
|
123
104
|
return {
|
|
124
|
-
workflow_input:
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
105
|
+
workflow_input: workflowInput,
|
|
106
|
+
cursor_update: latestSource === undefined
|
|
107
|
+
? {}
|
|
108
|
+
: {
|
|
109
|
+
refreshed_through_cursor: {
|
|
110
|
+
created_at: latestSource.created_at,
|
|
111
|
+
message_id: latestSource.id,
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
source_refs: sourceMessages.map((message) => ({
|
|
132
115
|
message_id: message.id,
|
|
133
|
-
source_scope:
|
|
116
|
+
source_scope: "main_chat",
|
|
134
117
|
created_at: message.created_at,
|
|
135
118
|
})),
|
|
136
|
-
source_message_count:
|
|
119
|
+
source_message_count: unrefreshedSourceCount,
|
|
137
120
|
};
|
|
138
121
|
}
|
|
139
122
|
export async function runControlPlaneScratchpadRefreshProcedure(options) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createMainChatAgentMessage, markScratchpadRefreshSucceeded, upsertScratchpadProjection, } from "../collaboration-state/index.js";
|
|
1
|
+
import { createMainChatAgentMessage, markScratchpadRefreshSucceeded, markScratchpadSourceDirty, upsertScratchpadProjection, } from "../collaboration-state/index.js";
|
|
2
2
|
import { mainChatInvalidates, scratchpadRefreshInvalidates, } from "./invalidations.js";
|
|
3
3
|
export function applyScratchpadRefreshOutput(input) {
|
|
4
4
|
if (input.output.decision === "needs_human") {
|
|
@@ -17,8 +17,10 @@ export function applyScratchpadRefreshOutput(input) {
|
|
|
17
17
|
invalidates: mainChatInvalidates(),
|
|
18
18
|
});
|
|
19
19
|
markScratchpadRefreshSucceeded(input.store.db, {
|
|
20
|
+
...input.cursorUpdate,
|
|
20
21
|
now: input.now,
|
|
21
22
|
});
|
|
23
|
+
markScratchpadSourceDirty(input.store.db, { now: input.now });
|
|
22
24
|
return {
|
|
23
25
|
kind: "needs_human",
|
|
24
26
|
summary: input.output.request_payload.summary,
|
|
@@ -1,10 +1,18 @@
|
|
|
1
|
-
import type { MessageProjection,
|
|
2
|
-
import type { ScratchpadRefreshSourceMessageInput,
|
|
1
|
+
import type { MessageProjection, WorklistProjection } from "@tutti/shared/schemas/api";
|
|
2
|
+
import type { ScratchpadRefreshSourceMessageInput, ScratchpadRefreshWorklistInput } from "./workflows/index.js";
|
|
3
|
+
export declare const MAX_SOURCE_MESSAGES_TOTAL_CHARS = 10000;
|
|
4
|
+
export declare const MIN_SOURCE_MESSAGE_TEXT_CHARS = 160;
|
|
5
|
+
export declare class ScratchpadSourceBudgetExceededError extends Error {
|
|
6
|
+
constructor();
|
|
7
|
+
}
|
|
3
8
|
export declare function shouldUseMessageAsScratchpadSource(message: MessageProjection): boolean;
|
|
4
|
-
export declare function shouldUseClarificationRoundMessageAsScratchpadSource(message: MessageProjection): boolean;
|
|
5
9
|
export declare function filterScratchpadSourceMessages(messages: MessageProjection[]): MessageProjection[];
|
|
6
|
-
export declare function
|
|
7
|
-
export declare function
|
|
8
|
-
|
|
9
|
-
|
|
10
|
+
export declare function truncateTextMiddle(text: string, maxChars: number): string;
|
|
11
|
+
export declare function compactScratchpadSourceMessages(messages: MessageProjection[], options?: {
|
|
12
|
+
totalTextChars?: number;
|
|
13
|
+
}): ScratchpadRefreshSourceMessageInput[];
|
|
14
|
+
export declare function compactScratchpadWorklist(worklist: WorklistProjection, options?: {
|
|
15
|
+
summaryChars?: number;
|
|
16
|
+
includeSummaries?: boolean;
|
|
17
|
+
}): ScratchpadRefreshWorklistInput;
|
|
10
18
|
//# sourceMappingURL=scratchpad-source-messages.d.ts.map
|
|
@@ -1,10 +1,19 @@
|
|
|
1
|
-
const MAX_COMPACT_SOURCE_MESSAGES = 20;
|
|
2
1
|
const MAX_SOURCE_MESSAGE_TEXT_CHARS = 2_000;
|
|
3
|
-
const MAX_SOURCE_MESSAGES_TOTAL_CHARS = 10_000;
|
|
2
|
+
export const MAX_SOURCE_MESSAGES_TOTAL_CHARS = 10_000;
|
|
3
|
+
export const MIN_SOURCE_MESSAGE_TEXT_CHARS = 160;
|
|
4
|
+
const MESSAGE_BOUNDARY = "\n\n--- next message ---\n\n";
|
|
5
|
+
const MIDDLE_OMISSION = "\n...[middle omitted by Tutti]...\n";
|
|
6
|
+
const MAX_WORKLIST_SUMMARY_CHARS = 800;
|
|
7
|
+
export class ScratchpadSourceBudgetExceededError extends Error {
|
|
8
|
+
constructor() {
|
|
9
|
+
super("Scratchpad source messages exceed the minimum deterministic context budget");
|
|
10
|
+
this.name = "ScratchpadSourceBudgetExceededError";
|
|
11
|
+
}
|
|
12
|
+
}
|
|
4
13
|
export function shouldUseMessageAsScratchpadSource(message) {
|
|
5
14
|
const explicitSource = message.refs?.scratchpad_source;
|
|
6
15
|
if (explicitSource !== undefined) {
|
|
7
|
-
return explicitSource === "include";
|
|
16
|
+
return message.scope.kind === "main_chat" && explicitSource === "include";
|
|
8
17
|
}
|
|
9
18
|
if (message.scope.kind !== "main_chat") {
|
|
10
19
|
return false;
|
|
@@ -17,47 +26,31 @@ export function shouldUseMessageAsScratchpadSource(message) {
|
|
|
17
26
|
}
|
|
18
27
|
return false;
|
|
19
28
|
}
|
|
20
|
-
export function shouldUseClarificationRoundMessageAsScratchpadSource(message) {
|
|
21
|
-
const explicitSource = message.refs?.scratchpad_source;
|
|
22
|
-
if (explicitSource !== undefined) {
|
|
23
|
-
return explicitSource === "include";
|
|
24
|
-
}
|
|
25
|
-
if (message.scope.kind !== "clarification_round") {
|
|
26
|
-
return false;
|
|
27
|
-
}
|
|
28
|
-
if (message.message_kind === "clarification_request") {
|
|
29
|
-
return true;
|
|
30
|
-
}
|
|
31
|
-
return message.message_kind === "user_text" && message.author.kind === "human";
|
|
32
|
-
}
|
|
33
29
|
export function filterScratchpadSourceMessages(messages) {
|
|
34
30
|
return messages.filter(shouldUseMessageAsScratchpadSource);
|
|
35
31
|
}
|
|
36
|
-
export function filterClarificationRoundScratchpadSourceMessages(messages) {
|
|
37
|
-
return messages.filter(shouldUseClarificationRoundMessageAsScratchpadSource);
|
|
38
|
-
}
|
|
39
|
-
export function sortScratchpadSourceMessages(messages) {
|
|
40
|
-
return [...messages].sort((left, right) => {
|
|
41
|
-
const timeOrder = left.created_at.localeCompare(right.created_at);
|
|
42
|
-
return timeOrder === 0 ? left.id.localeCompare(right.id) : timeOrder;
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
32
|
function compactRole(message) {
|
|
46
33
|
return message.author.kind === "agent" ? "tutti" : "human";
|
|
47
34
|
}
|
|
48
|
-
function
|
|
35
|
+
export function truncateTextMiddle(text, maxChars) {
|
|
49
36
|
const trimmed = text.trim();
|
|
50
|
-
if (trimmed.length <=
|
|
37
|
+
if (trimmed.length <= maxChars) {
|
|
51
38
|
return trimmed;
|
|
52
39
|
}
|
|
53
|
-
|
|
40
|
+
if (maxChars < MIDDLE_OMISSION.length + 2) {
|
|
41
|
+
throw new ScratchpadSourceBudgetExceededError();
|
|
42
|
+
}
|
|
43
|
+
const remaining = maxChars - MIDDLE_OMISSION.length;
|
|
44
|
+
const headLength = Math.ceil(remaining / 2);
|
|
45
|
+
const tailLength = Math.floor(remaining / 2);
|
|
46
|
+
return `${trimmed.slice(0, headLength)}${MIDDLE_OMISSION}${trimmed.slice(-tailLength)}`;
|
|
54
47
|
}
|
|
55
|
-
function
|
|
48
|
+
function sourceText(message) {
|
|
56
49
|
const reference = message.refs?.reference_file;
|
|
57
50
|
if (reference === undefined) {
|
|
58
|
-
return
|
|
51
|
+
return message.body;
|
|
59
52
|
}
|
|
60
|
-
return
|
|
53
|
+
return `Reference file (${reference.category}:${reference.name})\n${message.body}`;
|
|
61
54
|
}
|
|
62
55
|
function sourceAuthorDisplayName(message) {
|
|
63
56
|
if (message.author.kind !== "human") {
|
|
@@ -66,14 +59,12 @@ function sourceAuthorDisplayName(message) {
|
|
|
66
59
|
const displayName = message.author.display_name?.trim();
|
|
67
60
|
return displayName === "" ? undefined : displayName;
|
|
68
61
|
}
|
|
69
|
-
function compactSourceMessage(message) {
|
|
70
|
-
const ref = sourceReferenceLabel(message);
|
|
62
|
+
function compactSourceMessage(message, maxChars) {
|
|
71
63
|
const authorDisplayName = sourceAuthorDisplayName(message);
|
|
72
64
|
return {
|
|
73
65
|
role: compactRole(message),
|
|
74
66
|
...(authorDisplayName === undefined ? {} : { author_display_name: authorDisplayName }),
|
|
75
|
-
text:
|
|
76
|
-
...(ref === undefined ? {} : { ref }),
|
|
67
|
+
text: truncateTextMiddle(sourceText(message), maxChars),
|
|
77
68
|
};
|
|
78
69
|
}
|
|
79
70
|
function mergeCompactSourceMessages(messages) {
|
|
@@ -82,50 +73,43 @@ function mergeCompactSourceMessages(messages) {
|
|
|
82
73
|
const previous = merged.at(-1);
|
|
83
74
|
if (previous !== undefined &&
|
|
84
75
|
previous.role === message.role &&
|
|
85
|
-
previous.ref === message.ref &&
|
|
86
76
|
previous.author_display_name === message.author_display_name) {
|
|
87
|
-
previous.text =
|
|
77
|
+
previous.text = `${previous.text}${MESSAGE_BOUNDARY}${message.text}`;
|
|
88
78
|
continue;
|
|
89
79
|
}
|
|
90
80
|
merged.push({ ...message });
|
|
91
81
|
}
|
|
92
82
|
return merged;
|
|
93
83
|
}
|
|
94
|
-
function
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
for (const message of messages.toReversed()) {
|
|
98
|
-
if (remaining <= 0) {
|
|
99
|
-
break;
|
|
100
|
-
}
|
|
101
|
-
const text = message.text.length <= remaining
|
|
102
|
-
? message.text
|
|
103
|
-
: `${message.text.slice(0, Math.max(0, remaining - 3))}...`;
|
|
104
|
-
result.push({
|
|
105
|
-
...message,
|
|
106
|
-
text,
|
|
107
|
-
});
|
|
108
|
-
remaining -= text.length;
|
|
84
|
+
export function compactScratchpadSourceMessages(messages, options = {}) {
|
|
85
|
+
if (messages.length === 0) {
|
|
86
|
+
return [];
|
|
109
87
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
export function compactPreviousScratchpad(scratchpad) {
|
|
116
|
-
if (scratchpad.state !== "ready") {
|
|
117
|
-
return {};
|
|
88
|
+
const totalTextChars = options.totalTextChars ?? MAX_SOURCE_MESSAGES_TOTAL_CHARS;
|
|
89
|
+
const boundaryBudget = MESSAGE_BOUNDARY.length * Math.max(0, messages.length - 1);
|
|
90
|
+
const availableTextChars = totalTextChars - boundaryBudget;
|
|
91
|
+
if (availableTextChars < messages.length * MIN_SOURCE_MESSAGE_TEXT_CHARS) {
|
|
92
|
+
throw new ScratchpadSourceBudgetExceededError();
|
|
118
93
|
}
|
|
94
|
+
const perMessageChars = Math.min(MAX_SOURCE_MESSAGE_TEXT_CHARS, Math.floor(availableTextChars / messages.length));
|
|
95
|
+
return mergeCompactSourceMessages(messages.map((message) => compactSourceMessage(message, perMessageChars)));
|
|
96
|
+
}
|
|
97
|
+
export function compactScratchpadWorklist(worklist, options = {}) {
|
|
98
|
+
const summaryChars = options.summaryChars ?? MAX_WORKLIST_SUMMARY_CHARS;
|
|
99
|
+
const includeSummaries = options.includeSummaries ?? true;
|
|
119
100
|
return {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
:
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
101
|
+
modules: worklist.modules
|
|
102
|
+
.filter((entry) => !entry.module.archived)
|
|
103
|
+
.map((entry) => ({
|
|
104
|
+
name: entry.module.name,
|
|
105
|
+
tasks: entry.tasks.map((task) => ({
|
|
106
|
+
title: task.title,
|
|
107
|
+
...(includeSummaries && task.summary.trim() !== ""
|
|
108
|
+
? { summary: truncateTextMiddle(task.summary, summaryChars) }
|
|
109
|
+
: {}),
|
|
110
|
+
status: task.status,
|
|
111
|
+
})),
|
|
112
|
+
})),
|
|
129
113
|
};
|
|
130
114
|
}
|
|
131
115
|
//# sourceMappingURL=scratchpad-source-messages.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ClarificationRoundRef, WorkflowInvocationRef } from "@tutti/shared/ids";
|
|
2
|
+
import type { HostProjectStore } from "../store/index.js";
|
|
3
|
+
import type { TaskCompileWorkflowInput } from "./workflows/index.js";
|
|
4
|
+
export declare function readTaskCompileClarificationRounds(input: {
|
|
5
|
+
store: HostProjectStore;
|
|
6
|
+
workflowRef: WorkflowInvocationRef;
|
|
7
|
+
throughRoundId: ClarificationRoundRef;
|
|
8
|
+
}): NonNullable<TaskCompileWorkflowInput["clarification_rounds"]>;
|
|
9
|
+
//# sourceMappingURL=task-compile-clarification-context.d.ts.map
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { readActiveClarificationProjection, readClarificationRoundProjection, readClarificationRoundsForThread, readMessageProjectionById, } from "../collaboration-state/index.js";
|
|
2
|
+
import { ScratchpadSourceBudgetExceededError, truncateTextMiddle } from "./scratchpad-source-messages.js";
|
|
3
|
+
const MAX_CLARIFICATION_MESSAGE_CHARS = 4_000;
|
|
4
|
+
const MAX_CLARIFICATION_MESSAGES_TOTAL_CHARS = 16_000;
|
|
5
|
+
const MIN_CLARIFICATION_MESSAGE_CHARS = 160;
|
|
6
|
+
function answerMessage(store, messageId, roundId) {
|
|
7
|
+
const message = readMessageProjectionById(store.db, messageId);
|
|
8
|
+
if (message === null ||
|
|
9
|
+
message.scope.kind !== "clarification_round" ||
|
|
10
|
+
message.scope.clarification_round_ref !== roundId ||
|
|
11
|
+
(message.message_kind !== "user_text" && message.message_kind !== "agent_text")) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
const role = message.author.kind === "agent" ? "tutti" : "human";
|
|
15
|
+
const displayName = message.author.display_name?.trim();
|
|
16
|
+
return {
|
|
17
|
+
role,
|
|
18
|
+
...(role === "human" && displayName !== undefined && displayName !== ""
|
|
19
|
+
? { author_display_name: displayName }
|
|
20
|
+
: {}),
|
|
21
|
+
text: message.body,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export function readTaskCompileClarificationRounds(input) {
|
|
25
|
+
const active = readActiveClarificationProjection(input.store.db);
|
|
26
|
+
if (active.state !== "active" ||
|
|
27
|
+
active.thread.owner.kind !== "task_compile" ||
|
|
28
|
+
active.thread.owner.workflow_ref !== input.workflowRef ||
|
|
29
|
+
active.round.id !== input.throughRoundId) {
|
|
30
|
+
throw new Error("Task Compile clarification owner does not match its pinned context");
|
|
31
|
+
}
|
|
32
|
+
const throughRound = readClarificationRoundProjection(input.store.db, input.throughRoundId);
|
|
33
|
+
if (throughRound === null || throughRound.status !== "sealed") {
|
|
34
|
+
throw new Error("Task Compile continuation requires a sealed clarification round");
|
|
35
|
+
}
|
|
36
|
+
const rounds = readClarificationRoundsForThread(input.store.db, throughRound.thread_id, {
|
|
37
|
+
throughRoundIndex: throughRound.round_index,
|
|
38
|
+
});
|
|
39
|
+
const contextRounds = rounds.map((round) => {
|
|
40
|
+
if (round.status !== "sealed") {
|
|
41
|
+
throw new Error("Task Compile clarification context cannot include a writable round");
|
|
42
|
+
}
|
|
43
|
+
const answerMessages = (round.submitted_message_refs ?? [])
|
|
44
|
+
.filter((messageId) => messageId !== round.opening_message_id && messageId !== round.card_message_id)
|
|
45
|
+
.flatMap((messageId) => {
|
|
46
|
+
const mapped = answerMessage(input.store, messageId, round.id);
|
|
47
|
+
return mapped === null ? [] : [mapped];
|
|
48
|
+
});
|
|
49
|
+
return {
|
|
50
|
+
request_payload: round.request_payload,
|
|
51
|
+
answer_messages: answerMessages,
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
const answerCount = contextRounds.reduce((count, round) => count + round.answer_messages.length, 0);
|
|
55
|
+
if (answerCount === 0) {
|
|
56
|
+
return contextRounds;
|
|
57
|
+
}
|
|
58
|
+
if (answerCount * MIN_CLARIFICATION_MESSAGE_CHARS > MAX_CLARIFICATION_MESSAGES_TOTAL_CHARS) {
|
|
59
|
+
throw new ScratchpadSourceBudgetExceededError();
|
|
60
|
+
}
|
|
61
|
+
const perMessageChars = Math.min(MAX_CLARIFICATION_MESSAGE_CHARS, Math.floor(MAX_CLARIFICATION_MESSAGES_TOTAL_CHARS / answerCount));
|
|
62
|
+
return contextRounds.map((round) => ({
|
|
63
|
+
request_payload: round.request_payload,
|
|
64
|
+
answer_messages: round.answer_messages.map((message) => ({
|
|
65
|
+
...message,
|
|
66
|
+
text: truncateTextMiddle(message.text, perMessageChars),
|
|
67
|
+
})),
|
|
68
|
+
}));
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=task-compile-clarification-context.js.map
|
|
@@ -1,12 +1,17 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { materializeClarificationSuccessor, readActiveTaskCompileContext, readClarificationRoundProjection, readWorklistProjection, rollbackTaskCompileContext, } from "../collaboration-state/index.js";
|
|
2
|
+
import { withHostStoreTransaction } from "../store/index.js";
|
|
2
3
|
import { clarificationInvalidates } from "./invalidations.js";
|
|
3
4
|
import { applyTaskCompileProcedureOutput } from "./task-compile-output.js";
|
|
5
|
+
import { readTaskCompileClarificationRounds } from "./task-compile-clarification-context.js";
|
|
4
6
|
import { logProcedureExecutionError } from "./procedure-logging.js";
|
|
5
7
|
export function startControlPlaneTaskCompileContinuation(options) {
|
|
6
8
|
if (options.owner.kind !== "task_compile") {
|
|
7
9
|
return false;
|
|
8
10
|
}
|
|
9
11
|
const workflowRef = options.owner.workflow_ref;
|
|
12
|
+
if (readActiveTaskCompileContext(options.store.db, workflowRef) === null) {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
10
15
|
const runnerResolution = options.resolveWorkflowRunner();
|
|
11
16
|
if (runnerResolution.kind !== "ready") {
|
|
12
17
|
return false;
|
|
@@ -29,28 +34,26 @@ export function startControlPlaneTaskCompileContinuation(options) {
|
|
|
29
34
|
execute: async (context) => {
|
|
30
35
|
const round = readClarificationRoundProjection(options.store.db, options.roundId);
|
|
31
36
|
try {
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
:
|
|
40
|
-
|
|
41
|
-
messages: messagesWindow?.items ?? [],
|
|
42
|
-
};
|
|
37
|
+
const taskCompileContext = readActiveTaskCompileContext(options.store.db, workflowRef);
|
|
38
|
+
if (taskCompileContext === null) {
|
|
39
|
+
throw new Error("Active Task Compile Context is unavailable");
|
|
40
|
+
}
|
|
41
|
+
const clarificationRounds = readTaskCompileClarificationRounds({
|
|
42
|
+
store: options.store,
|
|
43
|
+
workflowRef,
|
|
44
|
+
throughRoundId: options.roundId,
|
|
45
|
+
});
|
|
43
46
|
const output = await runnerResolution.runner.runTaskCompile({
|
|
44
47
|
workflow_ref: workflowRef,
|
|
45
|
-
scratchpad:
|
|
48
|
+
scratchpad: taskCompileContext.scratchpad_snapshot,
|
|
46
49
|
worklist: readWorklistProjection(options.store.db),
|
|
47
|
-
|
|
50
|
+
clarification_rounds: clarificationRounds,
|
|
48
51
|
}, {
|
|
49
52
|
workflow_ref: workflowRef,
|
|
50
53
|
activity_ref: context.activity_ref,
|
|
51
54
|
reportProgress: context.reportProgress,
|
|
52
55
|
});
|
|
53
|
-
|
|
56
|
+
return applyTaskCompileProcedureOutput({
|
|
54
57
|
store: options.store,
|
|
55
58
|
events: options.events,
|
|
56
59
|
output,
|
|
@@ -60,52 +63,21 @@ export function startControlPlaneTaskCompileContinuation(options) {
|
|
|
60
63
|
continueFromRoundId: options.roundId,
|
|
61
64
|
onTasksAdded: options.onTasksAdded,
|
|
62
65
|
});
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
const threadId = round?.thread_id;
|
|
69
|
+
withHostStoreTransaction(options.store.db, (tx) => {
|
|
70
|
+
materializeClarificationSuccessor(tx, {
|
|
68
71
|
round_id: options.roundId,
|
|
72
|
+
successor_ref: { kind: "chain_closed" },
|
|
73
|
+
close_thread: true,
|
|
74
|
+
closed_reason: "cancelled",
|
|
69
75
|
now: options.now,
|
|
70
76
|
});
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const threadId = round?.thread_id;
|
|
74
|
-
materializeClarificationSuccessor(options.store.db, {
|
|
75
|
-
round_id: options.roundId,
|
|
76
|
-
successor_ref: {
|
|
77
|
-
kind: "proposal_applied",
|
|
78
|
-
workflow_ref: workflowRef,
|
|
79
|
-
},
|
|
80
|
-
close_thread: true,
|
|
77
|
+
rollbackTaskCompileContext(tx, {
|
|
78
|
+
workflow_ref: workflowRef,
|
|
81
79
|
now: options.now,
|
|
82
80
|
});
|
|
83
|
-
if (threadId !== undefined) {
|
|
84
|
-
options.events.publish({
|
|
85
|
-
event_type: "clarification.changed",
|
|
86
|
-
payload: {
|
|
87
|
-
reason: "successor_materialized",
|
|
88
|
-
thread_id: threadId,
|
|
89
|
-
round_id: options.roundId,
|
|
90
|
-
successor_ref: {
|
|
91
|
-
kind: "proposal_applied",
|
|
92
|
-
workflow_ref: workflowRef,
|
|
93
|
-
},
|
|
94
|
-
},
|
|
95
|
-
invalidates: clarificationInvalidates(options.roundId, threadId),
|
|
96
|
-
});
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
return conclusion;
|
|
100
|
-
}
|
|
101
|
-
catch (error) {
|
|
102
|
-
const threadId = round?.thread_id;
|
|
103
|
-
materializeClarificationSuccessor(options.store.db, {
|
|
104
|
-
round_id: options.roundId,
|
|
105
|
-
successor_ref: { kind: "chain_closed" },
|
|
106
|
-
close_thread: true,
|
|
107
|
-
closed_reason: "cancelled",
|
|
108
|
-
now: options.now,
|
|
109
81
|
});
|
|
110
82
|
if (threadId !== undefined) {
|
|
111
83
|
options.events.publish({
|