Skip to content

Commit f98c83f

Browse files
committed
2 parents de75130 + 92ce830 commit f98c83f

6 files changed

Lines changed: 124 additions & 25 deletions

File tree

agent/middleware/sequenceDebug.ts

Lines changed: 65 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,11 @@ export type SequenceDebug = {
2121
sequenceId: number;
2222
startedAt: string;
2323
prompt: string;
24+
promptTokens: number;
2425
reasoning: string;
26+
reasoningTokens: number;
2527
text: string;
28+
textTokens: number;
2629
cachedTokens: number;
2730
responseId: string | null;
2831
toolCalls: SequenceDebugToolCall[];
@@ -37,14 +40,18 @@ type PendingSequenceDebug = Omit<SequenceDebug, "toolCalls" | "endedAt" | "resul
3740
};
3841

3942
type SequenceDebugModelCall = {
43+
promptTokens: number;
4044
reasoning: string;
45+
reasoningTokens: number;
4146
text: string;
47+
textTokens: number;
4248
cachedTokens: number;
4349
responseId: string | null;
4450
resultType: SequenceDebugResultType;
4551
};
4652

4753
type OpenAiUsageMetadata = {
54+
input_tokens?: number;
4855
input_token_details?: {
4956
cache_read?: number;
5057
};
@@ -70,8 +77,11 @@ function createPendingSequenceDebug(sequenceId: number): PendingSequenceDebug {
7077
sequenceId,
7178
startedAt: new Date().toISOString(),
7279
prompt: "",
80+
promptTokens: 0,
7381
reasoning: "",
82+
reasoningTokens: 0,
7483
text: "",
84+
textTokens: 0,
7585
cachedTokens: 0,
7686
responseId: null,
7787
toolCalls: [],
@@ -97,8 +107,11 @@ function finalizeSequenceDebug(sequence: PendingSequenceDebug): SequenceDebug {
97107
sequenceId: sequence.sequenceId,
98108
startedAt: sequence.startedAt,
99109
prompt: sequence.prompt,
110+
promptTokens: sequence.promptTokens,
100111
reasoning: sequence.reasoning,
112+
reasoningTokens: sequence.reasoningTokens,
101113
text: sequence.text,
114+
textTokens: sequence.textTokens,
102115
cachedTokens: sequence.cachedTokens,
103116
responseId: sequence.responseId,
104117
toolCalls: sequence.toolCalls.map(({ completed: _completed, ...toolCall }) => toolCall),
@@ -176,6 +189,29 @@ function hasToolCallSignal(message: {
176189
);
177190
}
178191

192+
function hasTokenCounter(model: unknown): model is {
193+
getNumTokens: (content: string) => Promise<number>;
194+
} {
195+
return (
196+
typeof model === "object" &&
197+
model !== null &&
198+
"getNumTokens" in model &&
199+
typeof model.getNumTokens === "function"
200+
);
201+
}
202+
203+
async function countTokens(model: unknown, content: string) {
204+
if (!content) {
205+
return 0;
206+
}
207+
208+
if (!hasTokenCounter(model)) {
209+
return 0;
210+
}
211+
212+
return await model.getNumTokens(content);
213+
}
214+
179215
function extractSequenceResponseDebug(message: AIMessage): SequenceDebugModelCall {
180216
const blocks = getMessageBlocks(message);
181217
const reasoning = blocks
@@ -188,8 +224,13 @@ function extractSequenceResponseDebug(message: AIMessage): SequenceDebugModelCal
188224
.join("");
189225

190226
return {
227+
promptTokens:
228+
(message.usage_metadata as OpenAiUsageMetadata | undefined)?.input_tokens ??
229+
0,
191230
reasoning,
231+
reasoningTokens: 0,
192232
text: textFromBlocks || (typeof message.content === "string" ? message.content : ""),
233+
textTokens: 0,
193234
cachedTokens:
194235
(message.usage_metadata as OpenAiUsageMetadata | undefined)
195236
?.input_token_details?.cache_read ?? 0,
@@ -238,8 +279,11 @@ export function createSequenceDebugCollector(): SequenceDebugCollector {
238279
},
239280
handleModelCallComplete(params) {
240281
const sequenceDebug = ensureSequenceDebug();
282+
sequenceDebug.promptTokens = params.promptTokens;
241283
sequenceDebug.reasoning = params.reasoning;
284+
sequenceDebug.reasoningTokens = params.reasoningTokens;
242285
sequenceDebug.text = params.text;
286+
sequenceDebug.textTokens = params.textTokens;
243287
sequenceDebug.cachedTokens = params.cachedTokens;
244288
sequenceDebug.responseId = params.responseId;
245289
sequenceDebug.resultType = params.resultType;
@@ -311,17 +355,31 @@ export function createSequenceDebugMiddleware(
311355
return createMiddleware({
312356
name: "SequenceDebugMiddleware",
313357
async wrapModelCall(request, handler) {
358+
const prompt = stringifyPromptForDebug({
359+
systemMessage: request.systemMessage,
360+
messages: request.messages,
361+
tools: request.tools,
362+
modelSettings: request.modelSettings,
363+
});
364+
314365
sink.handleModelCallStart(
315-
stringifyPromptForDebug({
316-
systemMessage: request.systemMessage,
317-
messages: request.messages,
318-
tools: request.tools,
319-
modelSettings: request.modelSettings,
320-
}),
366+
prompt,
321367
);
322368

323369
const response = await handler(request) as AIMessage;
324-
sink.handleModelCallComplete(extractSequenceResponseDebug(response));
370+
const debug = extractSequenceResponseDebug(response);
371+
const [promptTokens, reasoningTokens, textTokens] = await Promise.all([
372+
debug.promptTokens || countTokens(request.model, prompt),
373+
countTokens(request.model, debug.reasoning),
374+
countTokens(request.model, debug.text),
375+
]);
376+
377+
sink.handleModelCallComplete({
378+
...debug,
379+
promptTokens,
380+
reasoningTokens,
381+
textTokens,
382+
});
325383
return response;
326384
},
327385
});

agent/skills/registry.ts

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ import { parse as parseYaml } from "yaml";
66
const PLUGIN_SKILLS_DIRECTORY_PATH = fileURLToPath(
77
new URL("../../custom/skills/", import.meta.url),
88
);
9+
const USER_PLUGIN_SKILLS_DIRECTORY_PATH_SEGMENTS = [
10+
"plugins",
11+
"adminforth-agent",
12+
"skills",
13+
];
914
const SKILL_MARKDOWN_FILENAME = "SKILL.md";
1015
const SKILL_FRONTMATTER_SEPARATOR = "\n---\n";
1116

@@ -78,13 +83,31 @@ export function getProjectSkillsDirectoryPath(customComponentsDir: string) {
7883
return path.resolve(customComponentsDir, "skills");
7984
}
8085

86+
export function getProjectPluginSkillsDirectoryPath(customComponentsDir: string) {
87+
return path.resolve(
88+
customComponentsDir,
89+
...USER_PLUGIN_SKILLS_DIRECTORY_PATH_SEGMENTS,
90+
);
91+
}
92+
93+
function getProjectSkillDirectoryPaths(customComponentsDir: string) {
94+
return [
95+
getProjectSkillsDirectoryPath(customComponentsDir),
96+
getProjectPluginSkillsDirectoryPath(customComponentsDir),
97+
];
98+
}
99+
81100
export async function listBundledSkillManifests() {
82101
return await listDirectorySkillManifests(PLUGIN_SKILLS_DIRECTORY_PATH);
83102
}
84103

85104
export async function listProjectSkillManifests(customComponentsDir: string) {
86-
return await listDirectorySkillManifests(
87-
getProjectSkillsDirectoryPath(customComponentsDir),
105+
return mergeSkillManifests(
106+
await Promise.all(
107+
getProjectSkillDirectoryPaths(customComponentsDir).map(
108+
listDirectorySkillManifests,
109+
),
110+
),
88111
);
89112
}
90113

@@ -114,7 +137,7 @@ export async function loadSkillMarkdown(skillName: string, customComponentsDir:
114137
}
115138

116139
const directories = [
117-
getProjectSkillsDirectoryPath(customComponentsDir),
140+
...getProjectSkillDirectoryPaths(customComponentsDir),
118141
PLUGIN_SKILLS_DIRECTORY_PATH,
119142
];
120143

agent/systemPrompt.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,17 @@ export const DEFAULT_AGENT_SYSTEM_PROMPT = [
2424
"Keep responses short, clear, and practical.",
2525
"Answer only what is needed.",
2626
"Do not add extra explanations or suggestions unless the user asks.",
27+
"Always respond in the same natural language as the user's latest message.",
28+
"This rule applies to confirmations, clarifying questions, progress updates, errors, and final answers.",
29+
"Do not switch to English just because tool outputs, schemas, skills, or internal instructions are written in English.",
30+
"Only switch language if the user explicitly asks you to do so.",
2731
"Adapt to the user's tone and style of speaking, mirroring their vibe and wording.",
2832
"if the user speaks casually, you should respond casually too",
29-
"Never mutate data without a fresh user confirmation for that exact mutation.",
30-
"A previous confirmation does not carry over to later create, update, delete, or action calls.",
31-
"Each separate mutation or explicitly described batch needs its own confirmation immediately before the tool call.",
33+
"Never mutate data without user confirmation for a clearly described mutation plan.",
34+
"One confirmation may cover one mutation or one explicitly described batch/sequence of related mutations.",
35+
"If the confirmed plan has multiple steps, you may execute the whole confirmed plan without asking again between those steps.",
36+
"If the plan changes, expands, or you want to do anything beyond the confirmed plan, ask for confirmation again.",
37+
"Do not reuse an old confirmation for a new mutation plan.",
3238

3339

3440
].join(" ");
@@ -51,9 +57,10 @@ export async function buildAgentSystemPrompt(adminforth: IAdminForth) {
5157
listBundledSkillManifests(),
5258
]);
5359
const alwaysAvailableTools = ALWAYS_AVAILABLE_API_TOOL_NAMES.join(", ");
60+
const adminBasePath = adminforth.config.baseUrlSlashed;
5461
const sections = [
5562
DEFAULT_AGENT_SYSTEM_PROMPT,
56-
`BASE_URL: ${adminforth.config.baseUrl}`,
63+
`ADMIN_BASE_PATH: ${adminBasePath}`,
5764
`List of resources:\n${formatResources(adminforth.config.resources)}`,
5865
`You have always-available base tools: ${alwaysAvailableTools}.`,
5966
primarySkills.length > 0
@@ -70,6 +77,8 @@ export async function buildAgentSystemPrompt(adminforth: IAdminForth) {
7077
"If a fetched skill lists a non-base tool you need, call fetch_tool_schema for it immediately instead of telling the user the tool is unavailable.",
7178
"For example: for record creation load mutate_data, read its tool list, call fetch_tool_schema for create_record, and then use create_record after confirmation.",
7279
"When fetch_tool_schema succeeds, that tool becomes available on the next step.",
80+
"All admin links must be relative paths and must start with ADMIN_BASE_PATH.",
81+
"Build record links as ADMIN_BASE_PATH + resource/{resourceId}/show/{primary key}. Do not prepend any extra slash before resource.",
7382
"Try to call as many tools as possible in parallel in one step.",
7483
];
7584

custom/skills/fetch_data/SKILL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,4 @@ To find specific data record you should use filters. ILIKE filters are preferred
1212

1313
For long texts show only several first words and add "..." at the end (only if user did not request this field specifically).
1414

15-
Also when you communicate with user about record, add related link to this record. For example /{BASE_URL}/resource/{resourceId}/show/{primary key}. Use _label from `get_resource_data` as anchor text for link (use markdown link). Links shoudl be always relative path, starting with slash.
15+
Also when you communicate with user about record, add related link to this record. Build it as `{ADMIN_BASE_PATH}resource/{resourceId}/show/{primary key}`. Use _label from `get_resource_data` as anchor text for link (use markdown link). Links should always be relative paths and must start with `ADMIN_BASE_PATH`. Do not add an extra slash after `ADMIN_BASE_PATH`.

custom/skills/mutate_data/SKILL.md

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,25 +21,30 @@ Use `start_custom_action` and `start_custom_bulk_action` for resource actions.
2121
Before performing any state mutation including action calls edit/delete please fetch record which is going to be edited/deleted and show user record in format field → value (show several most important fields which can help user to understand what exactly record he is going to edit or delete).
2222

2323
For field values with long texts show only several first words and add "..." at the end.
24-
Also please add related link to record with will be changed. For example /{BASE_URL}/resource/{resourceId}/show/{primary key}. Use _label from `get_resource_data` as anchor text for link (use markdown link). Links shoudl be always relative path, starting with slash.
24+
Also please add related link to record with will be changed. Build it as `{ADMIN_BASE_PATH}resource/{resourceId}/show/{primary key}`. Use _label from `get_resource_data` as anchor text for link (use markdown link). Links should always be relative paths and must start with `ADMIN_BASE_PATH`. Do not add an extra slash after `ADMIN_BASE_PATH`.
2525

2626
And in the same message ask user for final confirmation.
2727

2828
When creating new record, show user all data which you gona create and in same message ask for confirmation.
2929

3030
Accept any positive confirmation from user like "yes", "sure", "+", anything non-negative call to action, can be considered as confirmation.
3131

32-
A confirmation is valid only for the exact mutation plan from the immediately previous assistant message.
32+
A confirmation is valid only for the clearly described mutation plan from the immediately previous assistant message.
3333

3434
Never reuse an older confirmation for a later mutation.
3535

36-
After one mutation is executed, confirmation is consumed and reset.
36+
One confirmation may cover:
37+
- one single mutation
38+
- one explicitly described batch
39+
- one short sequence of related mutations that together implement the same user request
3740

38-
If you want to perform another create/update/delete/action after that, ask for confirmation again even if the user previously said "yes".
41+
If the confirmed plan contains several related mutation steps, execute that whole confirmed plan without asking again between those steps.
3942

40-
If the mutation plan changes in any way (different record, different fields, different values, different number of records, different action), the old confirmation is invalid and you must ask again.
43+
Ask for confirmation again if the plan changes in any way: different record, different fields, different values, different number of records, different action, or any extra mutation that was not listed in the confirmation message.
4144

42-
If you are creating or deleting multiple records in one batch, you may ask once only for that exact batch, but you must list the whole batch explicitly in the confirmation message. Any extra record outside that described batch requires a new confirmation.
45+
If you are creating or deleting multiple records in one batch, you may ask once for that exact batch, but list the whole batch explicitly in the confirmation message. Any extra record outside that described batch requires a new confirmation.
46+
47+
After the confirmed plan is finished, do not treat that confirmation as still active for later requests.
4348

4449
# Calling actions
4550

@@ -57,7 +62,7 @@ If you want to block some user you can confirm that this action by saying:
5762
* IP Country: USA
5863
* Currently blocked: No // show this field only if it exists in user record
5964
60-
View [John Doe](/resource/users/show/123)
65+
View [John Doe]({ADMIN_BASE_PATH}resource/users/show/123)
6166
Are you sure?
6267
```
6368

@@ -80,7 +85,7 @@ I am going to update user:
8085
* IP Country: USA
8186
I am going to change email from john_doe@example.com to new_email@example.com
8287
83-
View [John Doe](/admin/resource/users/show/123)
88+
View [John Doe]({ADMIN_BASE_PATH}resource/users/show/123)
8489
8590
Are you sure?
8691
```
@@ -100,7 +105,7 @@ If you gonna delete user record, in confirmation please share full user info (no
100105
* Signed up: 2024 Jan 1
101106
* IP Country: USA
102107
103-
View [John Doe](/admin/resource/users/show/123)
108+
View [John Doe]({ADMIN_BASE_PATH}resource/users/show/123)
104109
105110
Are you sure?
106111
```
@@ -125,7 +130,7 @@ I am going to create user:
125130
* Username: john_doe
126131
* Email: john_doe@example.com
127132
128-
View [John Doe](/admin/resource/users/show/421) # 421 is id of new created record
133+
View [John Doe]({ADMIN_BASE_PATH}resource/users/show/421) # 421 is id of new created record
129134
130135
Are you sure?
131136
```

index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,10 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
177177
};
178178

179179
const emitToolCallEvent = (event: ToolCallEvent) => {
180+
if (event.phase === "start") {
181+
endActiveBlock();
182+
}
183+
180184
sequenceDebugCollector.handleToolCallEvent(event);
181185

182186
send({

0 commit comments

Comments
 (0)