@mercury-fw/formatter 0.70.0 → 0.71.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.
- package/CHANGELOG.md +19 -0
- package/README.md +3 -1
- package/dist/index.d.ts +23 -11
- package/index.ts +88 -43
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# @mercury-fw/formatter
|
|
2
2
|
|
|
3
|
+
## 0.71.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 2b08f7e: - `formatterPlugin` renders the `display` of every tool a plugin contributes, typed tools included (`{ ok: true, …, display }`)
|
|
8
|
+
- formatter rule `{ item, empty?, always: true }`: that kind is shown after the answer without `present`, the model gets `shown: true` next to its `displayRef`
|
|
9
|
+
- `ToolDisplay.always`
|
|
10
|
+
- the core strips `display` from every plugin tool's result and stashes it, a typed tool's too
|
|
11
|
+
- stashed list labels: the tool name, its input and the item count
|
|
12
|
+
- a failed or cancelled turn's displays are no longer appended to the next answer
|
|
13
|
+
- `present` is offered with any plugin tool
|
|
14
|
+
- **Breaking:** `createCliTool` no longer takes `stashDisplay` and returns the post-processor's `display` as it is: drop the option
|
|
15
|
+
- **Breaking:** a hand-written `DisplayHandler` returns `{ text, always? }` instead of a string: wrap the text
|
|
16
|
+
|
|
17
|
+
### Patch Changes
|
|
18
|
+
|
|
19
|
+
- Updated dependencies [2b08f7e]
|
|
20
|
+
- @mercury-fw/plugin-types@0.71.0
|
|
21
|
+
|
|
3
22
|
## 0.70.0
|
|
4
23
|
|
|
5
24
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -24,6 +24,8 @@ export default defineMercuryConfig({
|
|
|
24
24
|
```
|
|
25
25
|
|
|
26
26
|
- `formatter<D>(rules)` takes one rule per kind of list, keyed by the kinds the plugin declares (`D`, e.g. `JiraDisplays`), so a kind it doesn't emit or a rule for the wrong item shape fails the typecheck. A rule is the line for each item, or `{ item, empty }` when an empty list needs a text of its own. Items are joined with a blank line. A rule also gets `t`, the texts of the person the turn is for, as its second argument (`(issue, t) => …`), and `empty` can be a function of it (`(t) => t("app.jira.no-issues")`, the key in the app's `locales/en.json` and its other language files), so an app that speaks several languages writes its lists in each person's.
|
|
27
|
-
-
|
|
27
|
+
- A rule in object form can also say `always: true`, for a kind that is the answer itself (a link the person asked for): Mercury shows it after the answer whether or not the model calls `present`, and tells the model it's shown so it doesn't write it again.
|
|
28
|
+
- `formatterPlugin(plugin, handler)` wraps a plugin so every list its tools hand over goes through the rules, a command-line tool's or one written in code alike (a tool hands one over by returning `{ ok: true, …, display: { type, items } }`); everything else the plugin contributes passes through untouched.
|
|
29
|
+
- A handler written by hand (instead of `formatter`) returns `{ text }`, or `{ text, always: true }`, or `undefined` when there's nothing to show.
|
|
28
30
|
|
|
29
31
|
MIT
|
package/dist/index.d.ts
CHANGED
|
@@ -8,20 +8,30 @@
|
|
|
8
8
|
*
|
|
9
9
|
* - `formatter(rules)` builds a `DisplayHandler` from a `kind → rule` map.
|
|
10
10
|
* - `formatterPlugin(plugin, handler)` wraps a data plugin so every display
|
|
11
|
-
* its
|
|
12
|
-
* plugin contributes passes through untouched.
|
|
11
|
+
* its tools return goes through the handler, a CLI tool's or a typed one's
|
|
12
|
+
* alike; everything else the plugin contributes passes through untouched.
|
|
13
13
|
*/
|
|
14
14
|
import type { Plugin, ToolDisplay, Translate } from "@mercury-fw/plugin-types";
|
|
15
|
-
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
export type
|
|
15
|
+
/** What a handler renders one display into: the single text block shown to the
|
|
16
|
+
* user, and `always` when the display is the answer itself, shown without the
|
|
17
|
+
* model calling `present`. */
|
|
18
|
+
export type RenderedDisplay = {
|
|
19
|
+
text: string;
|
|
20
|
+
always?: true;
|
|
21
|
+
};
|
|
22
|
+
/** Renders one display, in the turn's language (`t`), or returns `undefined`
|
|
23
|
+
* when there is nothing to show (no rule for its kind, or an empty list with no
|
|
24
|
+
* empty text). */
|
|
25
|
+
export type DisplayHandler = (display: ToolDisplay, t: Translate) => RenderedDisplay | undefined;
|
|
19
26
|
/** How one kind of list is rendered: the line for each item, or that plus the
|
|
20
|
-
* text to show when the list is empty
|
|
21
|
-
*
|
|
27
|
+
* text to show when the list is empty and whether the display is always shown
|
|
28
|
+
* (`always`: the answer itself, e.g. a link the person asked for, appended
|
|
29
|
+
* without the model calling `present`). The texts may use `t`, the texts of
|
|
30
|
+
* the person the turn is for (an app's own keys in its language files). */
|
|
22
31
|
export type FormatterRule<T> = ((item: T, t: Translate) => string) | {
|
|
23
32
|
item: (item: T, t: Translate) => string;
|
|
24
33
|
empty?: string | ((t: Translate) => string);
|
|
34
|
+
always?: true;
|
|
25
35
|
};
|
|
26
36
|
/**
|
|
27
37
|
* Builds a handler from the instance's rules. `D` is the data plugin's map of
|
|
@@ -33,10 +43,12 @@ export declare function formatter<D>(rules: {
|
|
|
33
43
|
[K in keyof D]?: FormatterRule<D[K]>;
|
|
34
44
|
}): DisplayHandler;
|
|
35
45
|
/**
|
|
36
|
-
* Wraps `plugin` so its
|
|
46
|
+
* Wraps `plugin` so the displays its tools return are rendered through
|
|
37
47
|
* `handler`. Returns a plugin identical to `plugin` except for a `build` that
|
|
38
48
|
* runs the inner `build` (passing the runtime context straight through) and
|
|
39
|
-
* wraps
|
|
40
|
-
*
|
|
49
|
+
* wraps its `sessionTools` factory: each tool it builds keeps every field but
|
|
50
|
+
* `execute`, whose output goes through the handler with the turn's texts. A
|
|
51
|
+
* CLI tool's display (attached by its post-processor) and a typed tool's take
|
|
52
|
+
* the same path; the post-processor itself is left as it is.
|
|
41
53
|
*/
|
|
42
54
|
export declare function formatterPlugin(plugin: Plugin, handler: DisplayHandler): Plugin;
|
package/index.ts
CHANGED
|
@@ -8,22 +8,29 @@
|
|
|
8
8
|
*
|
|
9
9
|
* - `formatter(rules)` builds a `DisplayHandler` from a `kind → rule` map.
|
|
10
10
|
* - `formatterPlugin(plugin, handler)` wraps a data plugin so every display
|
|
11
|
-
* its
|
|
12
|
-
* plugin contributes passes through untouched.
|
|
11
|
+
* its tools return goes through the handler, a CLI tool's or a typed one's
|
|
12
|
+
* alike; everything else the plugin contributes passes through untouched.
|
|
13
13
|
*/
|
|
14
|
-
import type {
|
|
14
|
+
import type { ExecutableTool, Plugin, PluginRuntimeContributions, ToolDisplay, Translate } from "@mercury-fw/plugin-types";
|
|
15
15
|
|
|
16
|
-
/**
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
export type
|
|
16
|
+
/** What a handler renders one display into: the single text block shown to the
|
|
17
|
+
* user, and `always` when the display is the answer itself, shown without the
|
|
18
|
+
* model calling `present`. */
|
|
19
|
+
export type RenderedDisplay = { text: string; always?: true };
|
|
20
|
+
|
|
21
|
+
/** Renders one display, in the turn's language (`t`), or returns `undefined`
|
|
22
|
+
* when there is nothing to show (no rule for its kind, or an empty list with no
|
|
23
|
+
* empty text). */
|
|
24
|
+
export type DisplayHandler = (display: ToolDisplay, t: Translate) => RenderedDisplay | undefined;
|
|
20
25
|
|
|
21
26
|
/** How one kind of list is rendered: the line for each item, or that plus the
|
|
22
|
-
* text to show when the list is empty
|
|
23
|
-
*
|
|
27
|
+
* text to show when the list is empty and whether the display is always shown
|
|
28
|
+
* (`always`: the answer itself, e.g. a link the person asked for, appended
|
|
29
|
+
* without the model calling `present`). The texts may use `t`, the texts of
|
|
30
|
+
* the person the turn is for (an app's own keys in its language files). */
|
|
24
31
|
export type FormatterRule<T> =
|
|
25
32
|
| ((item: T, t: Translate) => string)
|
|
26
|
-
| { item: (item: T, t: Translate) => string; empty?: string | ((t: Translate) => string) };
|
|
33
|
+
| { item: (item: T, t: Translate) => string; empty?: string | ((t: Translate) => string); always?: true };
|
|
27
34
|
|
|
28
35
|
/** Items are rendered one per block, separated by a blank line. */
|
|
29
36
|
const ITEM_SEPARATOR = "\n\n";
|
|
@@ -41,58 +48,96 @@ export function formatter<D>(rules: { [K in keyof D]?: FormatterRule<D[K]> }): D
|
|
|
41
48
|
if (rule === undefined) {
|
|
42
49
|
return undefined;
|
|
43
50
|
}
|
|
44
|
-
const { item, empty } = typeof rule === "function" ? { item: rule, empty: undefined } : rule;
|
|
45
|
-
|
|
46
|
-
|
|
51
|
+
const { item, empty, always } = typeof rule === "function" ? { item: rule, empty: undefined, always: undefined } : rule;
|
|
52
|
+
const text =
|
|
53
|
+
display.items.length === 0
|
|
54
|
+
? typeof empty === "function"
|
|
55
|
+
? empty(t)
|
|
56
|
+
: empty
|
|
57
|
+
: display.items.map((i) => item(i, t)).join(ITEM_SEPARATOR);
|
|
58
|
+
if (text === undefined) {
|
|
59
|
+
return undefined;
|
|
47
60
|
}
|
|
48
|
-
return
|
|
61
|
+
return always ? { text, always } : { text };
|
|
49
62
|
};
|
|
50
63
|
}
|
|
51
64
|
|
|
52
|
-
/**
|
|
53
|
-
*
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
function
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
65
|
+
/** A tool's output carrying a display to render: an ok result (`ok: true`)
|
|
66
|
+
* with a top-level `display`. */
|
|
67
|
+
type DisplayOutput = Record<string, unknown> & { ok: true; display: ToolDisplay };
|
|
68
|
+
|
|
69
|
+
/** Whether `output` is an ok result with a `display` (see `DisplayOutput`). */
|
|
70
|
+
function hasDisplay(output: unknown): output is DisplayOutput {
|
|
71
|
+
if (typeof output !== "object" || output === null) return false;
|
|
72
|
+
const o = output as Record<string, unknown>;
|
|
73
|
+
const display = o.display as ToolDisplay | undefined;
|
|
74
|
+
return o.ok === true && typeof display === "object" && display !== null && Array.isArray(display.items);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Applies `handler` to an ok output's display. A rendered block replaces the
|
|
78
|
+
* structured items (marked `always` when the handler says so); an empty list
|
|
79
|
+
* with nothing to show drops the display; a non-empty list with no rule stays
|
|
80
|
+
* structured (so it isn't shown) and is reported through `log`. Any other
|
|
81
|
+
* output is returned as it is, without calling the handler. */
|
|
82
|
+
function renderDisplay(output: unknown, handler: DisplayHandler, t: Translate, log: (m: string) => void, pluginName: string): unknown {
|
|
83
|
+
if (!hasDisplay(output)) {
|
|
84
|
+
return output;
|
|
66
85
|
}
|
|
67
|
-
const
|
|
68
|
-
if (
|
|
69
|
-
|
|
86
|
+
const rendered = handler(output.display, t);
|
|
87
|
+
if (rendered !== undefined) {
|
|
88
|
+
const display: ToolDisplay = { type: output.display.type, items: [rendered.text] };
|
|
89
|
+
if (rendered.always) display.always = true;
|
|
90
|
+
return { ...output, display };
|
|
70
91
|
}
|
|
71
|
-
if (
|
|
72
|
-
const { display: _dropped, ...rest } =
|
|
92
|
+
if (output.display.items.length === 0) {
|
|
93
|
+
const { display: _dropped, ...rest } = output;
|
|
73
94
|
return rest;
|
|
74
95
|
}
|
|
75
|
-
log(`[formatter] no rule for "${
|
|
76
|
-
return
|
|
96
|
+
log(`[formatter] no rule for "${output.display.type}" lists from plugin "${pluginName}" — list not shown`);
|
|
97
|
+
return output;
|
|
77
98
|
}
|
|
78
99
|
|
|
79
100
|
/**
|
|
80
|
-
* Wraps `plugin` so its
|
|
101
|
+
* Wraps `plugin` so the displays its tools return are rendered through
|
|
81
102
|
* `handler`. Returns a plugin identical to `plugin` except for a `build` that
|
|
82
103
|
* runs the inner `build` (passing the runtime context straight through) and
|
|
83
|
-
* wraps
|
|
84
|
-
*
|
|
104
|
+
* wraps its `sessionTools` factory: each tool it builds keeps every field but
|
|
105
|
+
* `execute`, whose output goes through the handler with the turn's texts. A
|
|
106
|
+
* CLI tool's display (attached by its post-processor) and a typed tool's take
|
|
107
|
+
* the same path; the post-processor itself is left as it is.
|
|
85
108
|
*/
|
|
86
109
|
export function formatterPlugin(plugin: Plugin, handler: DisplayHandler): Plugin {
|
|
87
110
|
return {
|
|
88
111
|
...plugin,
|
|
89
112
|
build: (ctx) => {
|
|
90
113
|
const inner = plugin.build ? plugin.build(ctx) : {};
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
114
|
+
const factory = inner.sessionTools;
|
|
115
|
+
if (factory === undefined) {
|
|
116
|
+
return inner;
|
|
117
|
+
}
|
|
118
|
+
const sessionTools: PluginRuntimeContributions["sessionTools"] = (sctx, postProcess) => {
|
|
119
|
+
const tools = factory(sctx, postProcess);
|
|
120
|
+
const wrapped: typeof tools = {};
|
|
121
|
+
for (const [name, tool] of Object.entries(tools)) {
|
|
122
|
+
const { execute } = tool;
|
|
123
|
+
wrapped[name] =
|
|
124
|
+
execute === undefined
|
|
125
|
+
? tool
|
|
126
|
+
: ({
|
|
127
|
+
...tool,
|
|
128
|
+
execute: async (input: unknown, options: unknown) =>
|
|
129
|
+
renderDisplay(
|
|
130
|
+
await (execute as (i: unknown, o: unknown) => unknown)(input, options),
|
|
131
|
+
handler,
|
|
132
|
+
sctx.t,
|
|
133
|
+
ctx.log,
|
|
134
|
+
plugin.name,
|
|
135
|
+
),
|
|
136
|
+
} as ExecutableTool);
|
|
137
|
+
}
|
|
138
|
+
return wrapped;
|
|
139
|
+
};
|
|
140
|
+
return { ...inner, sessionTools };
|
|
96
141
|
},
|
|
97
142
|
};
|
|
98
143
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mercury-fw/formatter",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.71.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"typecheck": "tsc --noEmit"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@mercury-fw/plugin-types": "0.
|
|
32
|
+
"@mercury-fw/plugin-types": "0.71.0"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"@mercury-fw/typescript-config": "*",
|