@adminforth/agent 2.2.5 → 2.3.1

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.
@@ -34,6 +34,14 @@ export const useAgentStore = defineStore('agent', () => {
34
34
  const textInput = ref<HTMLTextAreaElement | null>(null);
35
35
  const userMessageInput = ref();
36
36
  const trimmedUserMessage = computed(() => userMessageInput.value ? userMessageInput.value.trim() : '');
37
+ // Message-edit state. `editingEnabled` mirrors the server capability (checkpointer +
38
+ // turn checkpoints). While a message is being edited its id/turnId are held here and
39
+ // the user's in-progress input is stashed in `savedUserInput` so it can be restored.
40
+ const editingEnabled = ref(false);
41
+ const editingMessageId = ref<string | null>(null);
42
+ const editingMessageTurnId = ref<string | null>(null);
43
+ const savedUserInput = ref('');
44
+ const isEditingMessage = computed(() => editingMessageId.value !== null);
37
45
  const lastMessage = ref('');
38
46
  const isTeleportedToBody = ref(false);
39
47
  const setIsTeleportedToBody = (isTeleported: boolean) => {
@@ -55,6 +63,7 @@ export const useAgentStore = defineStore('agent', () => {
55
63
  setCurrentChat,
56
64
  abortCurrentChatRequest,
57
65
  submitToolApproval: submitToolApprovalResponse,
66
+ sendEditMessage,
58
67
  } = createAgentChatManager({
59
68
  lastMessage,
60
69
  activeModeName,
@@ -275,6 +284,10 @@ export const useAgentStore = defineStore('agent', () => {
275
284
  }
276
285
  })
277
286
  watch(activeSessionId, (newVal: string | null) => {
287
+ // The edited message lives in the outgoing session's chat, so drop edit mode.
288
+ if (editingMessageId.value !== null) {
289
+ exitEditMode();
290
+ }
278
291
  if (newVal) {
279
292
  setLocalStorageItem('lastSessionId', newVal);
280
293
  }
@@ -448,6 +461,67 @@ export const useAgentStore = defineStore('agent', () => {
448
461
  addSystemMessage(RESERVED_SYSTEM_MESSAGE_CONTENT.AGENT_RESPONSE_ABORTED);
449
462
  }
450
463
 
464
+ function setEditingEnabled(enabled: boolean) {
465
+ editingEnabled.value = enabled;
466
+ }
467
+
468
+ // Enter edit mode for a user message: stash the current input, then load the
469
+ // message's text into the textarea for the user to amend. Allowed mid-generation —
470
+ // committing the edit stops the running turn (see submitEditMessage).
471
+ function startEditMessage(message: any) {
472
+ if (!editingEnabled.value) {
473
+ return;
474
+ }
475
+ const turnId = message?.metadata?.turnId;
476
+ const messageId = message?.id;
477
+ if (!turnId || !messageId) {
478
+ return;
479
+ }
480
+ if (editingMessageId.value === null) {
481
+ savedUserInput.value = userMessageInput.value ?? '';
482
+ }
483
+ const text = (message.parts ?? [])
484
+ .filter((part: any) => part.type === 'text')
485
+ .map((part: any) => part.text ?? '')
486
+ .join('');
487
+ editingMessageId.value = messageId;
488
+ editingMessageTurnId.value = turnId;
489
+ userMessageInput.value = text;
490
+ nextTick(() => focusTextInput());
491
+ }
492
+
493
+ // Leave edit mode and restore whatever the user had been typing before.
494
+ function exitEditMode() {
495
+ userMessageInput.value = savedUserInput.value;
496
+ savedUserInput.value = '';
497
+ editingMessageId.value = null;
498
+ editingMessageTurnId.value = null;
499
+ }
500
+
501
+ function cancelEditMessage() {
502
+ if (editingMessageId.value === null) {
503
+ return;
504
+ }
505
+ exitEditMode();
506
+ nextTick(() => focusTextInput());
507
+ }
508
+
509
+ async function submitEditMessage() {
510
+ if (editingMessageId.value === null) {
511
+ return;
512
+ }
513
+ const text = trimmedUserMessage.value;
514
+ if (!text) {
515
+ return;
516
+ }
517
+ const messageId = editingMessageId.value;
518
+ const turnId = editingMessageTurnId.value!;
519
+ steerBuffer.clear();
520
+ // Restore the stashed input and leave edit mode before the regeneration streams in.
521
+ exitEditMode();
522
+ await sendEditMessage({ messageId, turnId, text });
523
+ }
524
+
451
525
  function resolveInternalRoute(href: string): string | null {
452
526
  if (href.startsWith('#')) {
453
527
  return `${window.location.pathname}${window.location.search}${href}`;
@@ -533,6 +607,15 @@ export const useAgentStore = defineStore('agent', () => {
533
607
  getLocalStorageItem,
534
608
  addDebugMessage,
535
609
  abortCurrentChatRequestAndAddSystemMessage,
610
+ //_________-Message editing-_____________
611
+ editingEnabled,
612
+ setEditingEnabled,
613
+ isEditingMessage,
614
+ editingMessageId,
615
+ startEditMessage,
616
+ cancelEditMessage,
617
+ submitEditMessage,
618
+ //_______________________________________
536
619
  addSystemMessage,
537
620
  isAudioChatMode,
538
621
  setIsAudioChatMode,
@@ -1,33 +1,77 @@
1
1
  <template>
2
- <div
3
- v-if="message.role === 'user' && message.metadata?.steer"
4
- class="self-end mr-4 mb-0.5 text-xs italic text-gray-400 dark:text-gray-500"
2
+ <div
3
+ class="relative flex flex-col w-full group/msg"
5
4
  >
6
- {{ $t('Steering instruction') }}
7
- </div>
8
- <ProcessingTimeline
9
- :message="message"
10
- :isLastMessageInChat="isLastMessageInChat"
11
- />
12
- <template
13
- v-for="(part, index) in getMessageParts(message)"
14
- :key="part.type"
15
- >
16
- <TextRenderer
17
- v-if="part.type === 'text' && !checkIfMessageSystemMessage(part.text ?? '')"
18
- :message="part.text"
19
- :role="props.message.role"
20
- :state="part.state ?? (props.message.role === 'user' ? 'done' : undefined)"
21
- />
22
- <ToolApprovalRenderer
23
- v-else-if="part.type === 'data-tool-approval'"
24
- :data="part.data ?? {}"
5
+ <div
6
+ v-if="canEdit"
7
+ class="absolute w-full h-[calc(100%+1.5rem)] pb-8"
8
+ @mouseenter="showEditButtonOnHover"
9
+ @mouseleave="hideEditButtonOnHover"
10
+ />
11
+ <Transition
12
+ enter-active-class="transition ease-out duration-200"
13
+ enter-from-class="opacity-0 scale-95"
14
+ enter-to-class="opacity-100 scale-100"
15
+ leave-active-class="transition ease-in duration-150"
16
+ leave-from-class="opacity-100 scale-100"
17
+ leave-to-class="opacity-0 scale-95"
18
+ >
19
+ <div
20
+ v-if="canEdit && showEditButton"
21
+ class="absolute flex -bottom-6 right-0 self-end p-1 mr-4 mt-0.5 z-20"
22
+ @mouseenter="showEditButtonOnHover"
23
+ @mouseleave="hideEditButtonOnHover"
24
+ >
25
+ <button
26
+ type="button"
27
+ class="flex items-center justify-center w-8 h-8 rounded-md text-gray-400 hover:text-lightPrimary dark:hover:text-darkPrimary opacity-60 group-hover/msg:opacity-100 transition-opacity duration-150 hover:scale-110"
28
+ :title="$t('Copy message')"
29
+ @click="copyMessageToClipboard(message)"
30
+ >
31
+ <IconFileCopySolid class="w-5 h-5" />
32
+ </button>
33
+ <button
34
+ type="button"
35
+ class="flex items-center justify-center w-8 h-8 rounded-md text-gray-400 hover:text-lightPrimary dark:hover:text-darkPrimary opacity-60 group-hover/msg:opacity-100 transition-opacity duration-150 hover:scale-110"
36
+ :title="$t('Edit message')"
37
+ @click="agentStore.startEditMessage(message)"
38
+ >
39
+ <IconPenSolid class="w-5 h-5" />
40
+ </button>
41
+ </div>
42
+ </Transition>
43
+ <div
44
+ v-if="message.role === 'user' && message.metadata?.steer"
45
+ class="self-end mr-4 mb-0.5 text-xs italic text-gray-400 dark:text-gray-500"
46
+ >
47
+ {{ $t('Steering instruction') }}
48
+ </div>
49
+ <ProcessingTimeline
50
+ :message="message"
51
+ :isLastMessageInChat="isLastMessageInChat"
25
52
  />
26
- <SystemMessageRenderer
27
- v-else-if="part.type === 'text'"
28
- :message="part.text"
29
- />
30
- </template>
53
+ <template
54
+ v-for="(part, index) in getMessageParts(message)"
55
+ :key="part.type"
56
+ >
57
+ <TextRenderer
58
+ v-if="part.type === 'text' && !checkIfMessageSystemMessage(part.text ?? '')"
59
+ :message="part.text"
60
+ :role="props.message.role"
61
+ :highlight="isBeingEdited"
62
+ :state="part.state ?? (props.message.role === 'user' ? 'done' : undefined)"
63
+ />
64
+ <ToolApprovalRenderer
65
+ v-else-if="part.type === 'data-tool-approval'"
66
+ :data="part.data ?? {}"
67
+ />
68
+ <SystemMessageRenderer
69
+ v-else-if="part.type === 'text'"
70
+ :message="part.text"
71
+ />
72
+ </template>
73
+
74
+ </div>
31
75
 
32
76
  </template>
33
77
 
@@ -35,6 +79,8 @@
35
79
 
36
80
 
37
81
  <script setup lang="ts">
82
+ import { computed, ref } from 'vue';
83
+ import { IconPenSolid, IconFileCopySolid } from '@iconify-prerendered/vue-flowbite';
38
84
  import TextRenderer from './TextRenderer.vue';
39
85
  import type { IMessage } from '../types';
40
86
  import { getMessageParts } from '../utils';
@@ -42,14 +88,53 @@
42
88
  import SystemMessageRenderer from './SystemMessageRenderer.vue';
43
89
  import ToolApprovalRenderer from './ToolApprovalRenderer.vue';
44
90
  import { RESERVED_SYSTEM_MESSAGE_CONTENT } from '../composables/agentStore/constants';
91
+ import { useAgentStore } from '../composables/useAgentStore';
92
+ import { useAdminforth } from '@/adminforth';
45
93
 
46
- const props = defineProps<{
47
- message: IMessage
94
+ const props = defineProps<{
95
+ message: IMessage
48
96
  isLastMessageInChat: boolean
49
97
  }>();
50
98
 
99
+ const agentStore = useAgentStore();
100
+ const { alert } = useAdminforth();
101
+ const showEditButton = ref(false);
102
+ // A user message is editable once it carries a turnId (loaded from history or stamped
103
+ // after its turn finished). Steer sub-messages and other roles are never editable, and
104
+ // we hide the affordance entirely while another edit or a live turn is in flight.
105
+ const canEdit = computed(() =>
106
+ agentStore.editingEnabled
107
+ && props.message.role === 'user'
108
+ && !props.message.metadata?.steer
109
+ && Boolean(props.message.metadata?.turnId)
110
+ && !agentStore.isEditingMessage
111
+ );
112
+
113
+ const isBeingEdited = computed(() =>
114
+ Boolean(props.message.id) && props.message.id === agentStore.editingMessageId
115
+ );
116
+
51
117
  function checkIfMessageSystemMessage(message: IMessage): boolean {
52
118
  const isReserved = Object.values(RESERVED_SYSTEM_MESSAGE_CONTENT).includes(message as RESERVED_SYSTEM_MESSAGE_CONTENT);
53
119
  return isReserved;
54
120
  }
121
+
122
+ function showEditButtonOnHover() {
123
+ showEditButton.value = true;
124
+ }
125
+
126
+ function hideEditButtonOnHover() {
127
+ showEditButton.value = false;
128
+ }
129
+
130
+ function copyMessageToClipboard(message: IMessage) {
131
+ navigator.clipboard.writeText(message.parts[0].text ?? '')
132
+ .then(() => {
133
+ alert({ message: 'Message copied to clipboard', variant: 'success' });
134
+ })
135
+ .catch((error) => {
136
+ console.error('Failed to copy message to clipboard:', error);
137
+ alert({ message: 'Failed to copy message to clipboard', variant: 'danger' });
138
+ });
139
+ }
55
140
  </script>
@@ -1,18 +1,20 @@
1
1
  <template>
2
2
  <div
3
- class="flex items-center justify-center rounded-xl border px-6 border-gray-200 dark:border-gray-700 min-w-0"
3
+ class="flex items-center justify-center rounded-xl border px-6 border-gray-200 dark:border-gray-700 min-w-0 transition-shadow duration-150"
4
4
  @click="handleMarkdownLinkClick"
5
5
  :class="[
6
- hasVegaLite ? 'w-full my-2' : '',
6
+ hasVegaLite ? 'w-full my-2' : 'm-2',
7
7
  props.role === 'user' ? 'bg-lightListTableHeading dark:bg-darkListTableHeading self-end max-w-[80%] mr-4'
8
- : 'border-none self-start max-w-full'
8
+ : 'border-none self-start max-w-full',
9
+ props.highlight ? 'ring-2 ring-lightPrimary dark:ring-darkPrimary w-full max-w-[calc(100%-2rem)]' : ''
9
10
  ]"
10
11
  >
11
12
  <IncremarkContent
12
13
  class="text-wrap break-words w-full max-w-full"
13
14
  v-if="content"
14
- :content="content"
15
- :is-finished="isFinished"
15
+ :key="renderNonce"
16
+ :content="content"
17
+ :is-finished="isFinished"
16
18
  :components="incremarkComponents"
17
19
  :incremark-options="incremarkOptions"
18
20
  />
@@ -33,7 +35,8 @@
33
35
  const props = defineProps<{
34
36
  message: string | undefined,
35
37
  state: string | undefined,
36
- role: IMessage['role']
38
+ role: IMessage['role'],
39
+ highlight?: boolean
37
40
  }>();
38
41
 
39
42
  const emit = defineEmits(['toggle-thoughts']);
@@ -49,6 +52,13 @@
49
52
 
50
53
  const content = computed(() => props.message)
51
54
  const isFinished = computed(() => props.state === 'done')
55
+
56
+ const renderNonce = ref(0);
57
+ watch(content, (newContent, oldContent) => {
58
+ if (isFinished.value && newContent !== oldContent) {
59
+ renderNonce.value++;
60
+ }
61
+ });
52
62
  const hasVegaLite = computed(() => props.message?.includes('```vega-lite'))
53
63
  const isStateStreaming = computed(() => props.state === 'streaming')
54
64
 
@@ -27,6 +27,7 @@
27
27
  :isRecording="microphoneButtonMode === 'listen'"
28
28
  :amplitude="audioAmplitude"
29
29
  />
30
+ <Spinner v-if="isPolling" class="ml-2 w-4 h-4 text-lightButtonsText dark:text-darkButtonsText fill-lightButtonsBackground dark:fill-darkPrimary" />
30
31
  </div>
31
32
  <div v-else-if="microphoneButtonMode === 'generating'" class="flex items-center justify-center gap-2 text-white text-sm">
32
33
  <span class="w-3 h-3 bg-white rounded-sm" />
@@ -65,6 +66,7 @@ const hideAnimationDebounced = debounce(() => {
65
66
  const sendUserRecordDebounced = debounce(() => {
66
67
  sendRecordForTranscription();
67
68
  }, 500);
69
+ const isPolling = ref(false);
68
70
 
69
71
  const isAudioChatMode = computed(() => agentStore.isAudioChatMode);
70
72
 
@@ -116,8 +118,10 @@ function toggleChatMode() {
116
118
  }
117
119
 
118
120
  async function onStartRecording() {
121
+ isPolling.value = true;
119
122
  await unlockAudio();
120
123
  await requestMicAndStartVAD(saidSomething, stopRecording, onAnySound);
124
+ isPolling.value = false;
121
125
  microphoneButtonMode.value = 'listen';
122
126
  agentAudio.playBeep(1000);
123
127
  }
@@ -30,11 +30,18 @@ export interface IToolGroup {
30
30
  }
31
31
 
32
32
  export interface IMessage {
33
+ id?: string;
33
34
  role: 'user' | 'assistant' | 'system';
34
35
  metadata?: any,
35
36
  parts: IPart[];
36
37
  }
37
38
 
39
+ export interface IStoredMessage {
40
+ role: 'user' | 'assistant' | 'system';
41
+ text: string;
42
+ turnId?: string;
43
+ }
44
+
38
45
  export interface IAgentSession {
39
46
  sessionId: string;
40
47
  title: string;
@@ -30,6 +30,10 @@ export type AgentEvent = {
30
30
  type: "transcript";
31
31
  text: string;
32
32
  language?: string;
33
+ } | {
34
+ type: "turn-persisted";
35
+ sessionId: string;
36
+ turnId: string;
33
37
  } | {
34
38
  type: "response";
35
39
  text: string;
@@ -63,6 +63,7 @@ export type RunAndPersistAgentResponseInput = BaseAgentTurnInput & {
63
63
  approvalDecision?: "approve" | "reject";
64
64
  failureLogMessage: string;
65
65
  abortLogMessage: string;
66
+ editTurnId?: string;
66
67
  };
67
68
  export type RunAndPersistAgentResponseResult = {
68
69
  text: string;
@@ -72,6 +73,9 @@ export type RunAndPersistAgentResponseResult = {
72
73
  };
73
74
  export type HandleTurnInput = TextAgentTurnInput;
74
75
  export type HandleSpeechTurnInput = SpeechAgentTurnInput;
76
+ export type HandleEditTurnInput = TextAgentTurnInput & {
77
+ turnId: string;
78
+ };
75
79
  /**
76
80
  * Provider-agnostic message passed from the application layer to the LLM port.
77
81
  * The infrastructure adapter maps it onto concrete LangChain messages.
package/dist/index.js CHANGED
@@ -79,6 +79,8 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
79
79
  getAdminforth: () => this.adminforth,
80
80
  getAgentSystemPrompt: this.getAgentSystemPrompt.bind(this),
81
81
  hasPersistentCheckpointer: Boolean(this.options.checkpointResource),
82
+ turnCheckpointsEnabled: Boolean(this.options.turnResource.checkpointIdField),
83
+ sessionResource: this.options.sessionResource,
82
84
  createDebugSink: createSequenceDebugCollector,
83
85
  });
84
86
  this.speechTurnService = new SpeechTurnService(this.runTurnUseCase.runAndPersistAgentResponse.bind(this.runTurnUseCase));
@@ -107,6 +109,7 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
107
109
  defaultModeName: this.options.modes[0].name,
108
110
  stickByDefault: (_b = this.options.stickByDefault) !== null && _b !== void 0 ? _b : false,
109
111
  hasAudioAdapter: Boolean(this.options.audioAdapter),
112
+ editingEnabled: Boolean(this.options.checkpointResource && this.options.turnResource.checkpointIdField),
110
113
  }
111
114
  });
112
115
  if (!this.adminforth.config.customization.customHeadItems) {
@@ -149,6 +152,7 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
149
152
  adminforth: this.adminforth,
150
153
  options: this.options,
151
154
  handleTurn: this.runTurnUseCase.handleTurn.bind(this.runTurnUseCase),
155
+ handleEditTurn: this.runTurnUseCase.handleEditTurn.bind(this.runTurnUseCase),
152
156
  handleSpeechTurn: this.speechTurnService.handle.bind(this.speechTurnService),
153
157
  steer: ({ sessionId, message }) => {
154
158
  const { id } = this.steerBuffer.add(sessionId, message);
@@ -21,6 +21,7 @@ export type AgentRuntimeRunInput = {
21
21
  } | Command;
22
22
  context: AgentTurnContext;
23
23
  observability: AgentTurnObservability;
24
+ branchFromCheckpointId?: string;
24
25
  };
25
26
  type AgentChatModelSpec = {
26
27
  model: AgentChatModel;
@@ -24,4 +24,18 @@ export declare class AgentRuntime {
24
24
  models: AgentTurnModels;
25
25
  sessionId: string;
26
26
  }): Promise<unknown[]>;
27
+ /**
28
+ * Return the thread's latest (tip) checkpoint id, or null when the thread has no
29
+ * persisted state. Builds a minimal agent and reads its state — no model call is
30
+ * made. Called after a turn completes to record the turn's fork point for editing.
31
+ */
32
+ getLatestCheckpointId(input: {
33
+ models: AgentTurnModels;
34
+ sessionId: string;
35
+ }): Promise<string | null>;
36
+ /**
37
+ * Drop the entire LangGraph checkpoint thread for a session. Used when editing the
38
+ * first message, where there is no earlier checkpoint to fork from.
39
+ */
40
+ resetThreadCheckpoints(sessionId: string): Promise<void>;
27
41
  }
@@ -68,9 +68,9 @@ export class AgentRuntime {
68
68
  recursionLimit: 100,
69
69
  callbacks: [createAgentLlmMetricsLogger()],
70
70
  signal: input.context.abortSignal,
71
- configurable: {
72
- thread_id: input.context.sessionId,
73
- },
71
+ configurable: Object.assign({ thread_id: input.context.sessionId }, (input.branchFromCheckpointId
72
+ ? { checkpoint_id: input.branchFromCheckpointId }
73
+ : {})),
74
74
  context: toLangchainAgentContext(Object.assign(Object.assign({}, input.context), { adminBaseUrl: adminforth.config.baseUrlSlashed, emit: input.observability.emit, sequenceDebugSink })),
75
75
  });
76
76
  });
@@ -104,4 +104,43 @@ export class AgentRuntime {
104
104
  return tasks.flatMap((task) => (Array.isArray(task === null || task === void 0 ? void 0 : task.interrupts) ? task.interrupts : []));
105
105
  });
106
106
  }
107
+ /**
108
+ * Return the thread's latest (tip) checkpoint id, or null when the thread has no
109
+ * persisted state. Builds a minimal agent and reads its state — no model call is
110
+ * made. Called after a turn completes to record the turn's fork point for editing.
111
+ */
112
+ getLatestCheckpointId(input) {
113
+ return __awaiter(this, void 0, void 0, function* () {
114
+ var _a, _b;
115
+ const apiBasedTools = this.options.toolProvider.getApiBasedTools();
116
+ const tools = yield this.options.toolProvider.getTools(apiBasedTools);
117
+ const agent = createAgent({
118
+ name: this.options.name,
119
+ model: input.models.model,
120
+ checkpointer: this.options.getCheckpointer(),
121
+ tools,
122
+ contextSchema,
123
+ middleware: [
124
+ humanInTheLoopMiddleware({
125
+ interruptOn: createHumanInTheLoopInterrupts(apiBasedTools),
126
+ descriptionPrefix: "Tool execution pending approval",
127
+ }),
128
+ ],
129
+ });
130
+ const state = yield agent.getState({
131
+ configurable: { thread_id: input.sessionId },
132
+ });
133
+ const checkpointId = (_b = (_a = state === null || state === void 0 ? void 0 : state.config) === null || _a === void 0 ? void 0 : _a.configurable) === null || _b === void 0 ? void 0 : _b.checkpoint_id;
134
+ return checkpointId ? String(checkpointId) : null;
135
+ });
136
+ }
137
+ /**
138
+ * Drop the entire LangGraph checkpoint thread for a session. Used when editing the
139
+ * first message, where there is no earlier checkpoint to fork from.
140
+ */
141
+ resetThreadCheckpoints(sessionId) {
142
+ return __awaiter(this, void 0, void 0, function* () {
143
+ yield this.options.getCheckpointer().deleteThread(sessionId);
144
+ });
145
+ }
107
146
  }
@@ -22,4 +22,12 @@ export declare class LangGraphLlm implements LlmPort {
22
22
  completionAdapter: AgentModeCompletionAdapter;
23
23
  sessionId: string;
24
24
  }): Promise<PendingInterrupt[]>;
25
+ getLatestCheckpointId(input: {
26
+ completionAdapter: AgentModeCompletionAdapter;
27
+ sessionId: string;
28
+ }): Promise<string | null>;
29
+ resetThreadCheckpoints(input: {
30
+ completionAdapter: AgentModeCompletionAdapter;
31
+ sessionId: string;
32
+ }): Promise<void>;
25
33
  }
@@ -37,6 +37,7 @@ export class LangGraphLlm {
37
37
  input: runtimeInput,
38
38
  context: input.context,
39
39
  observability: input.observability,
40
+ branchFromCheckpointId: input.branchFromCheckpointId,
40
41
  });
41
42
  return adaptRawStream(rawStream);
42
43
  });
@@ -51,4 +52,15 @@ export class LangGraphLlm {
51
52
  return normalizeInterrupts(raw);
52
53
  });
53
54
  }
55
+ getLatestCheckpointId(input) {
56
+ return __awaiter(this, void 0, void 0, function* () {
57
+ const models = yield this.modelFactory.create(input.completionAdapter);
58
+ return this.runtime.getLatestCheckpointId({ models, sessionId: input.sessionId });
59
+ });
60
+ }
61
+ resetThreadCheckpoints(input) {
62
+ return __awaiter(this, void 0, void 0, function* () {
63
+ yield this.runtime.resetThreadCheckpoints(input.sessionId);
64
+ });
65
+ }
54
66
  }
@@ -17,9 +17,33 @@ export declare class AgentSessionStore {
17
17
  */
18
18
  appendSteerToCurrentTurn(sessionId: string, steerText: string): Promise<void>;
19
19
  getSessionTurns(sessionId: string): Promise<{
20
+ id: any;
20
21
  prompt: any;
21
22
  response: any;
22
23
  }[]>;
24
+ /**
25
+ * Ordered non-system (real conversation) turns with their stored checkpoint id.
26
+ * Used by message editing to locate the target turn and its previous turn's
27
+ * fork point. Stable order (`createdAt ASC, id ASC`) so positions never drift.
28
+ */
29
+ getAgentTurns(sessionId: string): Promise<{
30
+ id: any;
31
+ prompt: any;
32
+ response: any;
33
+ checkpointId: string | null;
34
+ }[]>;
35
+ /**
36
+ * Apply a message edit and branch: replace the edited turn's prompt and truncate
37
+ * everything after it. Deletes later turns FIRST, then updates the edited turn, so
38
+ * an interruption can only leave a consistent "truncated, edit-not-yet-applied"
39
+ * state (recoverable by retry) — never "edited but stale later turns remain".
40
+ * (AdminForth's resource API has no transactions, hence the deliberate ordering.)
41
+ */
42
+ editTurnAndTruncateAfter(input: {
43
+ sessionId: string;
44
+ turnId: string;
45
+ newPrompt: string;
46
+ }): Promise<void>;
23
47
  getPreviousUserMessages(sessionId: string): Promise<PreviousUserMessage[]>;
24
48
  getLatestTurn(sessionId: string): Promise<any>;
25
49
  getChatSurfaceSessionId(incoming: ChatSurfaceIncomingMessage): string;
@@ -29,6 +53,12 @@ export declare class AgentSessionStore {
29
53
  turnId: string;
30
54
  responseText: string;
31
55
  debugHistory?: unknown;
56
+ /**
57
+ * Tip checkpoint id for this turn. Persisted only when provided AND the field is
58
+ * configured — so aborted/failed turns (which pass undefined) never overwrite it
59
+ * with a stale value.
60
+ */
61
+ checkpointId?: string | null;
32
62
  }): Promise<void>;
33
63
  getResumeState(sessionId: string): Promise<{
34
64
  turnId: string;