@adminforth/agent 2.9.1 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/README.md +4 -0
  2. package/dist/application/runTurnUseCase.d.ts +2 -1
  3. package/dist/application/runTurnUseCase.js +3 -3
  4. package/dist/custom/ChatFooter.vue +91 -104
  5. package/dist/custom/ChatHeader.vue +63 -62
  6. package/dist/custom/ChatSurface.vue +11 -18
  7. package/dist/custom/EditActionButtons.vue +12 -8
  8. package/dist/custom/SessionsHistory.vue +90 -28
  9. package/dist/custom/SteerQueue.vue +15 -9
  10. package/dist/custom/composables/agentStore/constants.ts +1 -1
  11. package/dist/custom/composables/agentStore/useAgentChat.ts +4 -3
  12. package/dist/custom/composables/agentStore/useAgentPlaceholder.ts +1 -0
  13. package/dist/custom/composables/agentStore/useAgentSessions.ts +23 -2
  14. package/dist/custom/composables/useAgentStore.ts +8 -29
  15. package/dist/custom/conversation_area/ConversationArea.vue +48 -16
  16. package/dist/custom/conversation_area/MessageRenderer.vue +18 -13
  17. package/dist/custom/conversation_area/ProcessingTimeline.vue +83 -123
  18. package/dist/custom/conversation_area/ReasoningRenderer.vue +13 -16
  19. package/dist/custom/conversation_area/SystemMessageRenderer.vue +4 -4
  20. package/dist/custom/conversation_area/TextRenderer.vue +67 -28
  21. package/dist/custom/conversation_area/ToolApprovalRenderer.vue +89 -56
  22. package/dist/custom/conversation_area/ToolRenderer.vue +44 -82
  23. package/dist/custom/incremark_code_renderers/IncremarkShikiCodeBlock.vue +22 -61
  24. package/dist/custom/speech_recognition_frontend/AudioLines.vue +1 -2
  25. package/dist/custom/speech_recognition_frontend/MicrophoneButon.vue +36 -32
  26. package/dist/custom/types.ts +16 -8
  27. package/dist/domain/agentEvents.d.ts +3 -2
  28. package/dist/tools/approvalFormatter.d.ts +19 -5
  29. package/dist/tools/approvalFormatter.js +88 -40
  30. package/dist/transport/http/context.d.ts +2 -1
  31. package/dist/transport/http/sessionEndpoints.js +34 -0
  32. package/package.json +3 -3
  33. package/dist/custom/SteerDebugPanel.vue +0 -45
  34. package/dist/custom/conversation_area/ToolsGroup.vue +0 -59
@@ -1,4 +1,5 @@
1
1
  import type { ToolCallEvent } from "./toolCallEvents.js";
2
+ import type { ApprovalDescription } from "../tools/approvalFormatter.js";
2
3
  export type AgentEvent = {
3
4
  type: "turn-started";
4
5
  messageId: string;
@@ -19,8 +20,8 @@ export type AgentEvent = {
19
20
  type: "interrupt";
20
21
  sessionId: string;
21
22
  interrupt: unknown;
22
- /** One plain-language line per pending tool call, for the approval card. */
23
- approvals: string[];
23
+ /** One description per pending tool call, for the approval card. */
24
+ approvals: ApprovalDescription[];
24
25
  } | {
25
26
  type: "steer-applied";
26
27
  count: number;
@@ -1,12 +1,26 @@
1
- import type { IAdminForth } from "adminforth";
1
+ import { type IAdminForth } from "adminforth";
2
2
  /** One tool call awaiting approval, normalized out of the provider interrupt payload. */
3
3
  export type ApprovalRequest = {
4
4
  toolName: string;
5
5
  args: Record<string, unknown>;
6
6
  };
7
+ /** One value on the approval card; `from` is the stored value an update replaces. */
8
+ export type ApprovalField = {
9
+ label: string;
10
+ from?: string;
11
+ to: string;
12
+ };
13
+ /** A tool call awaiting approval, described for the approval card. */
14
+ export type ApprovalDescription = {
15
+ /** What the call does and to which record, e.g. `Update record with id:10 in Cars`. */
16
+ summary: string;
17
+ /** The values the call writes, in the resource's own wording. */
18
+ fields: ApprovalField[];
19
+ };
7
20
  /**
8
- * Describe a tool call awaiting approval in a sentence an admin (not a developer) can
9
- * act on — resource and column labels instead of ids, values instead of raw JSON.
10
- * Falls back to a key/value dump for tool calls it does not recognize.
21
+ * Describe a tool call awaiting approval so an admin (not a developer) can act on it —
22
+ * resource and column labels instead of ids, values instead of raw JSON, and for an
23
+ * update the current value next to each new one. Tool calls it does not recognize are
24
+ * shown as their arguments.
11
25
  */
12
- export declare function formatApprovalRequest(adminforth: IAdminForth, request: ApprovalRequest): string;
26
+ export declare function formatApprovalRequest(adminforth: IAdminForth, request: ApprovalRequest): Promise<ApprovalDescription>;
@@ -1,7 +1,17 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ import { logger } from "adminforth";
11
+ import { getErrorMessage } from "../shared/errors.js";
1
12
  /** Argument keys that address the call itself rather than the data being changed. */
2
13
  const NON_FIELD_KEYS = new Set(["resourceId", "recordId", "primaryKey", "requiredColumnsToSkip", "meta", "extra"]);
3
14
  const MAX_VALUE_LENGTH = 80;
4
- const MAX_FIELDS = 12;
5
15
  function truncate(text, limit = MAX_VALUE_LENGTH) {
6
16
  return text.length > limit ? `${text.slice(0, limit - 1)}…` : text;
7
17
  }
@@ -45,23 +55,46 @@ function formatValue(value, column) {
45
55
  }
46
56
  return truncate(String(value));
47
57
  }
48
- /** `Brand: Tesla, Price: 45000` — the record payload in the resource's own wording. */
49
- function formatRecord(record, resource) {
58
+ /**
59
+ * The record payload as labelled values in the resource's own wording. Every field is
60
+ * listed: the admin approves exactly what will be written. With `oldRecord` each value is
61
+ * paired with the one it replaces.
62
+ */
63
+ function describeFields(record, resource, oldRecord) {
50
64
  var _a;
51
65
  if (record === null || typeof record !== "object" || Array.isArray(record)) {
52
- return "";
66
+ return [];
53
67
  }
54
68
  const columnsByName = new Map(((_a = resource === null || resource === void 0 ? void 0 : resource.columns) !== null && _a !== void 0 ? _a : []).map((column) => [column.name, column]));
55
- const entries = Object.entries(record)
56
- .filter(([key]) => !NON_FIELD_KEYS.has(key));
57
- const shown = entries.slice(0, MAX_FIELDS).map(([name, value]) => {
69
+ return Object.entries(record)
70
+ .filter(([key]) => !NON_FIELD_KEYS.has(key))
71
+ .map(([name, value]) => {
58
72
  const column = columnsByName.get(name);
59
- return `${(column === null || column === void 0 ? void 0 : column.label) || humanize(name)}: ${formatValue(value, column)}`;
73
+ return {
74
+ label: (column === null || column === void 0 ? void 0 : column.label) || humanize(name),
75
+ from: oldRecord ? formatValue(oldRecord[name], column) : undefined,
76
+ to: formatValue(value, column),
77
+ };
78
+ });
79
+ }
80
+ /**
81
+ * The stored record an update would change, read the same way the update endpoint reads it.
82
+ * The preview must never block the approval prompt, so a failed read only drops the old values.
83
+ */
84
+ function loadRecord(adminforth, resource, recordId) {
85
+ return __awaiter(this, void 0, void 0, function* () {
86
+ var _a;
87
+ if (!resource) {
88
+ return undefined;
89
+ }
90
+ try {
91
+ return (_a = yield adminforth.connectors[resource.dataSource].getRecordByPrimaryKey(resource, String(recordId))) !== null && _a !== void 0 ? _a : undefined;
92
+ }
93
+ catch (error) {
94
+ logger.warn(`Approval preview: failed to read record ${String(recordId)} of "${resource.resourceId}": ${getErrorMessage(error)}`);
95
+ return undefined;
96
+ }
60
97
  });
61
- const hidden = entries.length - shown.length;
62
- const lines = hidden > 0 ? [...shown, `and ${hidden} more`] : shown;
63
- // One field per line: a wide record is unreadable as a single run-on sentence.
64
- return lines.join(",\n");
65
98
  }
66
99
  function actionLabel(resource, actionId) {
67
100
  var _a, _b;
@@ -72,35 +105,50 @@ function actionLabel(resource, actionId) {
72
105
  return `"${(action === null || action === void 0 ? void 0 : action.name) || humanize(actionId)}"`;
73
106
  }
74
107
  /**
75
- * Describe a tool call awaiting approval in a sentence an admin (not a developer) can
76
- * act on — resource and column labels instead of ids, values instead of raw JSON.
77
- * Falls back to a key/value dump for tool calls it does not recognize.
108
+ * Describe a tool call awaiting approval so an admin (not a developer) can act on it —
109
+ * resource and column labels instead of ids, values instead of raw JSON, and for an
110
+ * update the current value next to each new one. Tool calls it does not recognize are
111
+ * shown as their arguments.
78
112
  */
79
113
  export function formatApprovalRequest(adminforth, request) {
80
- const { args, toolName } = request;
81
- const resource = findResource(adminforth, args.resourceId);
82
- const label = resourceLabel(resource, args);
83
- switch (toolName) {
84
- case "create_record": {
85
- const fields = formatRecord(args.record, resource);
86
- return `Create a new record in ${label}${fields ? `:\n${fields}` : ""}`;
87
- }
88
- case "update_record": {
89
- const fields = formatRecord(args.record, resource);
90
- return `Update record with id:${formatValue(args.recordId)} in ${label}${fields ? `, set:\n${fields}` : ""}`;
91
- }
92
- case "delete_record":
93
- return `Delete record with id:${formatValue(args.primaryKey)} from ${label} — this cannot be undone`;
94
- case "start_custom_action":
95
- return `Run ${actionLabel(resource, args.actionId)} on record ${formatValue(args.recordId)} in ${label}`;
96
- case "start_custom_bulk_action":
97
- case "start_bulk_action": {
98
- const count = Array.isArray(args.recordIds) ? args.recordIds.length : 0;
99
- return `Run ${actionLabel(resource, args.actionId)} on ${count} record(s) in ${label}`;
114
+ return __awaiter(this, void 0, void 0, function* () {
115
+ const { args, toolName } = request;
116
+ const resource = findResource(adminforth, args.resourceId);
117
+ const label = resourceLabel(resource, args);
118
+ switch (toolName) {
119
+ case "create_record":
120
+ return {
121
+ summary: `Create a new record in ${label}`,
122
+ fields: describeFields(args.record, resource),
123
+ };
124
+ case "update_record":
125
+ return {
126
+ summary: `Update record with id:${formatValue(args.recordId)} in ${label}`,
127
+ fields: describeFields(args.record, resource, yield loadRecord(adminforth, resource, args.recordId)),
128
+ };
129
+ case "delete_record":
130
+ return {
131
+ summary: `Delete record with id:${formatValue(args.primaryKey)} from ${label} — this cannot be undone`,
132
+ fields: [],
133
+ };
134
+ case "start_custom_action":
135
+ return {
136
+ summary: `Run ${actionLabel(resource, args.actionId)} on record ${formatValue(args.recordId)} in ${label}`,
137
+ fields: [],
138
+ };
139
+ case "start_custom_bulk_action":
140
+ case "start_bulk_action": {
141
+ const count = Array.isArray(args.recordIds) ? args.recordIds.length : 0;
142
+ return {
143
+ summary: `Run ${actionLabel(resource, args.actionId)} on ${count} record(s) in ${label}`,
144
+ fields: [],
145
+ };
146
+ }
147
+ default:
148
+ return {
149
+ summary: humanize(toolName),
150
+ fields: describeFields(args, resource),
151
+ };
100
152
  }
101
- default: {
102
- const fields = formatRecord(args, resource);
103
- return `${humanize(toolName)}${fields ? `:\n${fields}` : ""}`;
104
- }
105
- }
153
+ });
106
154
  }
@@ -1,6 +1,7 @@
1
1
  import type { ChatSurfaceAdapter, ChatSurfaceEventSink, ChatSurfaceIncomingMessage, IAdminForth, IAdminForthEndpointHandlerInput } from "adminforth";
2
2
  import type { HandleEditTurnInput, HandleSpeechTurnInput, HandleTurnInput, RunAndPersistAgentResponseInput, RunAndPersistAgentResponseResult } from "../../domain/turnTypes.js";
3
3
  import type { PluginOptions } from "../../types.js";
4
+ import type { ApprovalDescription } from "../../tools/approvalFormatter.js";
4
5
  export type SessionTurn = {
5
6
  id: string;
6
7
  prompt: string;
@@ -22,7 +23,7 @@ export type AgentEndpointsContext = {
22
23
  }>;
23
24
  runAndPersistAgentResponse(input: RunAndPersistAgentResponseInput): Promise<RunAndPersistAgentResponseResult>;
24
25
  getSessionTurns(sessionId: string): Promise<SessionTurn[]>;
25
- getPendingApprovals(sessionId: string): Promise<string[]>;
26
+ getPendingApprovals(sessionId: string): Promise<ApprovalDescription[]>;
26
27
  createNewTurn(sessionId: string, prompt: string, response?: string): Promise<string>;
27
28
  createSystemTurn(sessionId: string, systemMessage: string): Promise<string>;
28
29
  appendSteerToCurrentTurn(sessionId: string, steerText: string): Promise<void>;
@@ -28,6 +28,10 @@ const sessionIdBodySchema = z.object({
28
28
  const createSessionBodySchema = z.object({
29
29
  triggerMessage: z.string().optional(),
30
30
  }).strict();
31
+ const renameSessionBodySchema = z.object({
32
+ sessionId: z.string(),
33
+ title: z.string().trim().min(1).max(100),
34
+ }).strict();
31
35
  export function setupSessionEndpoints(ctx, server) {
32
36
  server.endpoint({
33
37
  method: 'POST',
@@ -175,6 +179,36 @@ export function setupSessionEndpoints(ctx, server) {
175
179
  };
176
180
  })
177
181
  });
182
+ server.endpoint({
183
+ method: 'POST',
184
+ path: `/agent/rename-session`,
185
+ agent: {
186
+ hiddenFromAgents: true,
187
+ },
188
+ request_schema: renameSessionBodySchema,
189
+ handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser, response }) {
190
+ const data = body;
191
+ const session = yield ctx.adminforth.resource(ctx.options.sessionResource.resourceId).get([Filters.EQ(ctx.options.sessionResource.idField, data.sessionId)]);
192
+ if (!session) {
193
+ response.setStatus(404, 'Session not found');
194
+ return {
195
+ error: 'Session not found'
196
+ };
197
+ }
198
+ if (session[ctx.options.sessionResource.askerIdField] !== adminUser.pk) {
199
+ response.setStatus(403, 'Unauthorized');
200
+ return {
201
+ error: 'Unauthorized'
202
+ };
203
+ }
204
+ yield ctx.adminforth.resource(ctx.options.sessionResource.resourceId).update(data.sessionId, {
205
+ [ctx.options.sessionResource.titleField]: data.title,
206
+ });
207
+ return {
208
+ title: data.title,
209
+ };
210
+ })
211
+ });
178
212
  server.endpoint({
179
213
  method: 'POST',
180
214
  path: `/agent/add-system-message-to-turns`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adminforth/agent",
3
- "version": "2.9.1",
3
+ "version": "3.0.0",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -36,7 +36,7 @@
36
36
  "devDependencies": {
37
37
  "@types/jest": "^30.0.0",
38
38
  "@types/node": "latest",
39
- "adminforth": "^3.32.0",
39
+ "adminforth": "^4.1.0",
40
40
  "jest": "^30.3.0",
41
41
  "semantic-release": "^24.2.1",
42
42
  "semantic-release-slack-bot": "^4.0.2",
@@ -80,6 +80,6 @@
80
80
  }
81
81
  ],
82
82
  "peerDependencies": {
83
- "adminforth": "^3.32.0"
83
+ "adminforth": "^4.1.0"
84
84
  }
85
85
  }
@@ -1,45 +0,0 @@
1
- <!--
2
- TEMPORARY debug helper: enqueues a test steer message into the buffer WITHOUT sending
3
- or steering it, so the SteerQueue enter/leave animation can be exercised without a live
4
- generating turn. Remove before shipping.
5
- -->
6
- <template>
7
- <div
8
- class="fixed bottom-[400px] left-4 z-[9999] flex items-center gap-2 rounded-lg border border-dashed border-amber-400 bg-amber-50/95 px-2.5 py-2 shadow-lg backdrop-blur dark:border-amber-500 dark:bg-amber-950/95"
9
- >
10
- <span class="text-[10px] font-bold uppercase tracking-wider text-amber-600 dark:text-amber-400">
11
- Steer&nbsp;debug
12
- </span>
13
- <input
14
- v-model="debugText"
15
- type="text"
16
- placeholder="Test steer text…"
17
- class="w-44 rounded-md border border-amber-300 bg-white px-2 py-1 text-xs text-gray-800 focus:outline-none focus:ring-1 focus:ring-amber-400 dark:border-amber-600 dark:bg-gray-800 dark:text-gray-100"
18
- @keydown.enter.prevent="addTestSteer"
19
- />
20
- <button
21
- type="button"
22
- class="rounded-md bg-amber-500 px-2.5 py-1 text-xs font-semibold text-white transition-colors hover:bg-amber-600 active:bg-amber-700"
23
- @click="addTestSteer"
24
- >
25
- Add steer
26
- </button>
27
- </div>
28
- </template>
29
-
30
- <script setup lang="ts">
31
- import { ref } from 'vue';
32
- import { useAgentStore } from './composables/useAgentStore';
33
-
34
- const agentStore = useAgentStore();
35
- const debugText = ref('');
36
- let counter = 0;
37
-
38
- function addTestSteer() {
39
- counter += 1;
40
- const text = debugText.value.trim() || `Test steer #${counter}`;
41
- // enqueue() only pushes onto the client-side buffer — no /agent/steer, no send.
42
- agentStore.enqueueSteerMessage(text);
43
- debugText.value = '';
44
- }
45
- </script>
@@ -1,59 +0,0 @@
1
- <template >
2
- <template v-if="toolGroup.length > 0">
3
- <li class="mb-6 ms-2 z-50">
4
- <span class="text-listTableHeadingText dark:text-darkListTableHeadingText bg-lightNavbar dark:bg-darkNavbar absolute flex items-center justify-center w-5 h-5 bg-brand-softer rounded-full -start-[0.68rem] ring-4 ring-lightNavbar dark:ring-darkNavbar ring-default">
5
- <div class="w-5 h-5 rounded-full flex items-center justify-center">
6
- <IconWrenchSolid class="w-4 h-4" />
7
- </div>
8
- </span>
9
- <h3
10
- class="flex items-center mb-1 text-sm my-2 ml-3 gap-1 text-listTableHeadingText dark:text-darkListTableHeadingText"
11
- >
12
- <span class="font-semibold select-none ">{{ $t('Call tools') }}</span>
13
- </h3>
14
- <div class="flex flex-wrap">
15
- <template v-for="group in props.toolGroup" :key="group.title">
16
- <ToolRenderer
17
- v-if="isExpanded"
18
- v-for="part in group.groupedTools"
19
- :key="part.toolInfo.toolCallId"
20
- :data="part"
21
- />
22
- </template>
23
- </div>
24
- </li>
25
- </template>
26
- </template>
27
-
28
- <script setup lang="ts">
29
- import ToolRenderer from './ToolRenderer.vue';
30
- import type { IToolGroup } from '../types';
31
- import { IconWrenchSolid } from '@iconify-prerendered/vue-heroicons';
32
-
33
- const props = defineProps<{
34
- toolGroup: IToolGroup[]
35
- isExpanded: boolean
36
- }>();
37
-
38
- </script>
39
-
40
- <style scoped>
41
-
42
- .expand-enter-active,
43
- .expand-leave-active {
44
- transition: all 0.3s ease;
45
- }
46
-
47
- .expand-enter-from,
48
- .expand-leave-to {
49
- opacity: 0;
50
- max-height: 0;
51
- }
52
-
53
- .expand-enter-to,
54
- .expand-leave-from {
55
- opacity: 1;
56
- max-height: 288px;
57
- }
58
-
59
- </style>