@datalayer/agent-runtimes 1.5.26 → 1.5.27
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-embed/datalayer-app-main.js +89 -89
- package/lib/chat/base/ChatBase.js +375 -437
- package/lib/chat/base/conversationItems.d.ts +55 -0
- package/lib/chat/base/conversationItems.js +126 -0
- package/lib/portable/adapterEvents.d.ts +107 -0
- package/lib/portable/adapterEvents.js +239 -0
- package/lib/portable/approvalCard.d.ts +56 -0
- package/lib/portable/approvalCard.js +56 -0
- package/lib/portable/conversation.d.ts +71 -18
- package/lib/portable/conversation.js +330 -74
- package/lib/portable/fixtures.js +12 -0
- package/lib/portable/index.d.ts +4 -1
- package/lib/portable/index.js +7 -1
- package/lib/portable/presence.js +4 -1
- package/lib/portable/runMeasures.d.ts +46 -0
- package/lib/portable/runMeasures.js +156 -0
- package/lib/protocols/ACPAdapter.js +13 -8
- package/lib/utils/utils.d.ts +2 -8
- package/lib/utils/utils.js +3 -26
- package/package.json +1 -1
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How a streamed turn went, measured without its words (MOBILE M-036, §10):
|
|
3
|
+
* the time to its first streamed token, to its complete answer, how long a
|
|
4
|
+
* Stop took, the category of a stream that failed, and how often a lost
|
|
5
|
+
* stream was read back. Never a prompt, an answer, a tool's arguments or an
|
|
6
|
+
* id — only durations, counts and coarse categories — and an OTLP/JSON body
|
|
7
|
+
* of them for the platform's metrics ingest.
|
|
8
|
+
*
|
|
9
|
+
* Pure: the clock is given.
|
|
10
|
+
*
|
|
11
|
+
* @module portable/runMeasures
|
|
12
|
+
*/
|
|
13
|
+
import { type AgUiEvent } from './agui';
|
|
14
|
+
/** Why a stream failed, coarsely. */
|
|
15
|
+
export type StreamFailure = 'refused' | 'cut_short' | 'network' | 'other';
|
|
16
|
+
/** One turn, measured. */
|
|
17
|
+
export type RunMeasure = {
|
|
18
|
+
surface: 'application' | 'agent';
|
|
19
|
+
firstTokenMs: number | null;
|
|
20
|
+
answerMs: number | null;
|
|
21
|
+
stopLatencyMs: number | null;
|
|
22
|
+
failure: StreamFailure | null;
|
|
23
|
+
};
|
|
24
|
+
/** The category of a stream's failure; never its message. */
|
|
25
|
+
export declare function failureOf(error: unknown): StreamFailure;
|
|
26
|
+
/** Measures one turn from its start: fed its events, its Stop, its failure; read once it ended. */
|
|
27
|
+
export declare class RunMeter {
|
|
28
|
+
private readonly surface;
|
|
29
|
+
private readonly now;
|
|
30
|
+
private readonly started;
|
|
31
|
+
private firstToken;
|
|
32
|
+
private answered;
|
|
33
|
+
private stopLatency;
|
|
34
|
+
private failed;
|
|
35
|
+
constructor(surface: RunMeasure['surface'], now?: () => number);
|
|
36
|
+
event(event: AgUiEvent): void;
|
|
37
|
+
stopped(latencyMs: number): void;
|
|
38
|
+
failure(error: unknown): void;
|
|
39
|
+
read(): RunMeasure;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The OTLP/JSON body of turns measured and lost streams read back: gauges of
|
|
43
|
+
* milliseconds and counters, each point with the surface and, for a failure,
|
|
44
|
+
* its category — nothing else.
|
|
45
|
+
*/
|
|
46
|
+
export declare function otlpMetricsOf(measures: RunMeasure[], recoveries: number, resource: Record<string, string>, nowMs: number): Record<string, unknown>;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2025-2026 Datalayer, Inc.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* How a streamed turn went, measured without its words (MOBILE M-036, §10):
|
|
7
|
+
* the time to its first streamed token, to its complete answer, how long a
|
|
8
|
+
* Stop took, the category of a stream that failed, and how often a lost
|
|
9
|
+
* stream was read back. Never a prompt, an answer, a tool's arguments or an
|
|
10
|
+
* id — only durations, counts and coarse categories — and an OTLP/JSON body
|
|
11
|
+
* of them for the platform's metrics ingest.
|
|
12
|
+
*
|
|
13
|
+
* Pure: the clock is given.
|
|
14
|
+
*
|
|
15
|
+
* @module portable/runMeasures
|
|
16
|
+
*/
|
|
17
|
+
import { RUN_CUT_SHORT } from './agui';
|
|
18
|
+
import { RuntimeRefusal } from './sessions';
|
|
19
|
+
/** The category of a stream's failure; never its message. */
|
|
20
|
+
export function failureOf(error) {
|
|
21
|
+
if (error instanceof RuntimeRefusal)
|
|
22
|
+
return 'refused';
|
|
23
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
24
|
+
if (message === RUN_CUT_SHORT || message.includes(RUN_CUT_SHORT))
|
|
25
|
+
return 'cut_short';
|
|
26
|
+
if (error instanceof TypeError ||
|
|
27
|
+
/network|offline|timed? ?out|fetch/i.test(message))
|
|
28
|
+
return 'network';
|
|
29
|
+
return 'other';
|
|
30
|
+
}
|
|
31
|
+
/** Measures one turn from its start: fed its events, its Stop, its failure; read once it ended. */
|
|
32
|
+
export class RunMeter {
|
|
33
|
+
surface;
|
|
34
|
+
now;
|
|
35
|
+
started;
|
|
36
|
+
firstToken = null;
|
|
37
|
+
answered = null;
|
|
38
|
+
stopLatency = null;
|
|
39
|
+
failed = null;
|
|
40
|
+
constructor(surface, now = () => Date.now()) {
|
|
41
|
+
this.surface = surface;
|
|
42
|
+
this.now = now;
|
|
43
|
+
this.started = now();
|
|
44
|
+
}
|
|
45
|
+
event(event) {
|
|
46
|
+
if (this.firstToken === null && event.type === 'TEXT_MESSAGE_CONTENT') {
|
|
47
|
+
this.firstToken = this.now() - this.started;
|
|
48
|
+
}
|
|
49
|
+
if (event.type === 'RUN_FINISHED' && this.answered === null) {
|
|
50
|
+
this.answered = this.now() - this.started;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
stopped(latencyMs) {
|
|
54
|
+
this.stopLatency = Math.max(0, latencyMs);
|
|
55
|
+
}
|
|
56
|
+
failure(error) {
|
|
57
|
+
this.failed = failureOf(error);
|
|
58
|
+
}
|
|
59
|
+
read() {
|
|
60
|
+
return {
|
|
61
|
+
surface: this.surface,
|
|
62
|
+
firstTokenMs: this.firstToken,
|
|
63
|
+
answerMs: this.failed || this.stopLatency !== null ? null : this.answered,
|
|
64
|
+
stopLatencyMs: this.stopLatency,
|
|
65
|
+
failure: this.failed,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const attributes = (values) => Object.entries(values).map(([key, value]) => ({
|
|
70
|
+
key,
|
|
71
|
+
value: { stringValue: value },
|
|
72
|
+
}));
|
|
73
|
+
/**
|
|
74
|
+
* The OTLP/JSON body of turns measured and lost streams read back: gauges of
|
|
75
|
+
* milliseconds and counters, each point with the surface and, for a failure,
|
|
76
|
+
* its category — nothing else.
|
|
77
|
+
*/
|
|
78
|
+
export function otlpMetricsOf(measures, recoveries, resource, nowMs) {
|
|
79
|
+
const time = String(BigInt(Math.trunc(nowMs)) * 1000000n);
|
|
80
|
+
const gauge = (name, pick) => ({
|
|
81
|
+
name,
|
|
82
|
+
unit: 'ms',
|
|
83
|
+
gauge: {
|
|
84
|
+
dataPoints: measures.flatMap(measure => {
|
|
85
|
+
const value = pick(measure);
|
|
86
|
+
return value === null
|
|
87
|
+
? []
|
|
88
|
+
: [
|
|
89
|
+
{
|
|
90
|
+
asDouble: value,
|
|
91
|
+
timeUnixNano: time,
|
|
92
|
+
attributes: attributes({ surface: measure.surface }),
|
|
93
|
+
},
|
|
94
|
+
];
|
|
95
|
+
}),
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
const failures = new Map();
|
|
99
|
+
for (const measure of measures) {
|
|
100
|
+
if (measure.failure) {
|
|
101
|
+
const key = `${measure.surface}\n${measure.failure}`;
|
|
102
|
+
failures.set(key, (failures.get(key) ?? 0) + 1);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const metrics = [
|
|
106
|
+
gauge('datalayer.client.run.first_token', measure => measure.firstTokenMs),
|
|
107
|
+
gauge('datalayer.client.run.answer', measure => measure.answerMs),
|
|
108
|
+
gauge('datalayer.client.run.stop_latency', measure => measure.stopLatencyMs),
|
|
109
|
+
{
|
|
110
|
+
name: 'datalayer.client.run.failures',
|
|
111
|
+
sum: {
|
|
112
|
+
aggregationTemporality: 1,
|
|
113
|
+
isMonotonic: true,
|
|
114
|
+
dataPoints: [...failures].map(([key, count]) => {
|
|
115
|
+
const [surface, category] = key.split('\n');
|
|
116
|
+
return {
|
|
117
|
+
asInt: String(count),
|
|
118
|
+
timeUnixNano: time,
|
|
119
|
+
attributes: attributes({ surface, category }),
|
|
120
|
+
};
|
|
121
|
+
}),
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
name: 'datalayer.client.run.recoveries',
|
|
126
|
+
sum: {
|
|
127
|
+
aggregationTemporality: 1,
|
|
128
|
+
isMonotonic: true,
|
|
129
|
+
dataPoints: recoveries > 0
|
|
130
|
+
? [
|
|
131
|
+
{
|
|
132
|
+
asInt: String(recoveries),
|
|
133
|
+
timeUnixNano: time,
|
|
134
|
+
attributes: [],
|
|
135
|
+
},
|
|
136
|
+
]
|
|
137
|
+
: [],
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
].filter(metric => 'gauge' in metric
|
|
141
|
+
? metric.gauge.dataPoints.length > 0
|
|
142
|
+
: metric.sum.dataPoints.length > 0);
|
|
143
|
+
return {
|
|
144
|
+
resourceMetrics: [
|
|
145
|
+
{
|
|
146
|
+
resource: { attributes: attributes(resource) },
|
|
147
|
+
scopeMetrics: [
|
|
148
|
+
{
|
|
149
|
+
scope: { name: '@datalayer/agent-runtimes/portable/runMeasures' },
|
|
150
|
+
metrics,
|
|
151
|
+
},
|
|
152
|
+
],
|
|
153
|
+
},
|
|
154
|
+
],
|
|
155
|
+
};
|
|
156
|
+
}
|
|
@@ -454,15 +454,20 @@ export class ACPAdapter extends BaseProtocolAdapter {
|
|
|
454
454
|
});
|
|
455
455
|
}
|
|
456
456
|
if (updateType === 'tool_call_update') {
|
|
457
|
+
// A call's result, once it says it ended; its progress before then is
|
|
458
|
+
// not a result. ACP names the call it updates.
|
|
457
459
|
const progress = update;
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
460
|
+
if (progress.status === 'completed' || progress.status === 'failed') {
|
|
461
|
+
this.emit({
|
|
462
|
+
type: 'tool-result',
|
|
463
|
+
toolResult: {
|
|
464
|
+
toolCallId: progress.toolCallId ?? '',
|
|
465
|
+
success: progress.status === 'completed',
|
|
466
|
+
result: progress.rawOutput,
|
|
467
|
+
},
|
|
468
|
+
timestamp: new Date(),
|
|
469
|
+
});
|
|
470
|
+
}
|
|
466
471
|
}
|
|
467
472
|
if (updateType === 'agent_thought_chunk') {
|
|
468
473
|
const thought = textOf(update.content);
|
package/lib/utils/utils.d.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* @module chat/utils
|
|
5
5
|
*/
|
|
6
6
|
import { QueryClient } from '@tanstack/react-query';
|
|
7
|
+
import { sanitizeAssistantContent } from '../portable/adapterEvents';
|
|
7
8
|
import type { ChatMessage } from '../types/messages';
|
|
8
9
|
import type { ProtocolConfig } from '../types/protocol';
|
|
9
10
|
import { type BaseProtocolAdapter } from '../protocols';
|
|
@@ -16,14 +17,7 @@ export declare const PRIMER_PORTAL_ROOT_ID = "__primerPortalRoot__";
|
|
|
16
17
|
* Check if an item is a tool call message
|
|
17
18
|
*/
|
|
18
19
|
export declare function isToolCallMessage(item: DisplayItem): item is ToolCallMessage;
|
|
19
|
-
|
|
20
|
-
* Strip low-level protocol/tool-call markup from assistant text content.
|
|
21
|
-
*
|
|
22
|
-
* Some models occasionally leak internal call formats (for example,
|
|
23
|
-
* <calls>...</calls> or <tml:invoke ...>) into natural-language responses.
|
|
24
|
-
* Keep user/system text untouched and sanitize assistant-visible text only.
|
|
25
|
-
*/
|
|
26
|
-
export declare function sanitizeAssistantContent(content: string): string;
|
|
20
|
+
export { sanitizeAssistantContent };
|
|
27
21
|
/**
|
|
28
22
|
* Extract text content from a ChatMessage
|
|
29
23
|
*/
|
package/lib/utils/utils.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* @module chat/utils
|
|
9
9
|
*/
|
|
10
10
|
import { QueryClient } from '@tanstack/react-query';
|
|
11
|
+
import { sanitizeAssistantContent } from '../portable/adapterEvents';
|
|
11
12
|
import { AGUIAdapter, A2AAdapter, VercelAIAdapter, ACPAdapter, BrowserAgentAdapter, } from '../protocols';
|
|
12
13
|
// ---------------------------------------------------------------------------
|
|
13
14
|
// Constants
|
|
@@ -32,32 +33,8 @@ export const PRIMER_PORTAL_ROOT_ID = '__primerPortalRoot__';
|
|
|
32
33
|
export function isToolCallMessage(item) {
|
|
33
34
|
return 'type' in item && item.type === 'tool-call';
|
|
34
35
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
*
|
|
38
|
-
* Some models occasionally leak internal call formats (for example,
|
|
39
|
-
* <calls>...</calls> or <tml:invoke ...>) into natural-language responses.
|
|
40
|
-
* Keep user/system text untouched and sanitize assistant-visible text only.
|
|
41
|
-
*/
|
|
42
|
-
export function sanitizeAssistantContent(content) {
|
|
43
|
-
if (!content || !content.includes('<')) {
|
|
44
|
-
return content;
|
|
45
|
-
}
|
|
46
|
-
let sanitized = content;
|
|
47
|
-
// Remove complete blocks first.
|
|
48
|
-
sanitized = sanitized
|
|
49
|
-
.replace(/<\s*calls\b[\s\S]*?<\s*\/\s*calls\s*>/gi, '')
|
|
50
|
-
.replace(/<\s*(?:an:)?tml:invoke\b[\s\S]*?<\s*\/\s*(?:an:)?tml:invoke\s*>/gi, '')
|
|
51
|
-
.replace(/<\s*(?:an:)?tml:parameter\b[\s\S]*?<\s*\/\s*(?:an:)?tml:parameter\s*>/gi, '');
|
|
52
|
-
// Remove standalone/open/close tags and malformed tag fragments.
|
|
53
|
-
sanitized = sanitized
|
|
54
|
-
.replace(/<\s*\/?\s*(?:an:[a-z_]+|tml:[a-z_]+|calls)\b[^>]*>?/gi, '')
|
|
55
|
-
.replace(/<\s*\/\s*>/g, '')
|
|
56
|
-
.replace(/^[ \t]+$/gm, '')
|
|
57
|
-
.replace(/\n{3,}/g, '\n\n')
|
|
58
|
-
.trim();
|
|
59
|
-
return sanitized;
|
|
60
|
-
}
|
|
36
|
+
// What a model leaked of its call markup, taken out: one rule for every client.
|
|
37
|
+
export { sanitizeAssistantContent };
|
|
61
38
|
/**
|
|
62
39
|
* Extract text content from a ChatMessage
|
|
63
40
|
*/
|