@cuylabs/agent-a365-tooling 3.1.0 → 4.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.
- package/README.md +86 -5
- package/dist/index.d.ts +128 -2
- package/dist/index.js +325 -0
- package/docs/lifecycle-and-limits.md +12 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -2,10 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
Microsoft Agent 365 tooling adapter for `@cuylabs/agent-core`.
|
|
4
4
|
|
|
5
|
-
This package exposes
|
|
6
|
-
|
|
5
|
+
This package exposes a turn-scoped tool provider factory and an optional
|
|
6
|
+
Agent 365 chat-history middleware. On each chat turn, the provider uses
|
|
7
|
+
Microsoft's Agent 365 tooling SDK to discover MCP servers for the active
|
|
7
8
|
Microsoft 365 turn, connects those servers through agent-core's MCP manager,
|
|
8
|
-
and returns MCP tools for that turn only.
|
|
9
|
+
and returns MCP tools for that turn only. The middleware can submit recent
|
|
10
|
+
agent-core chat history to Microsoft's Agent 365 real-time threat protection
|
|
11
|
+
registration endpoint.
|
|
9
12
|
|
|
10
13
|
Use this when your agent is hosted behind Microsoft 365 / Agent 365 and should
|
|
11
14
|
use the MCP tool servers configured for the current tenant, user, and agent
|
|
@@ -18,6 +21,8 @@ identity. Do not use it for static MCP servers known at startup; use
|
|
|
18
21
|
- Microsoft SDK token exchange and per-server bearer headers.
|
|
19
22
|
- Agent 365 platform headers for real Microsoft-hosted MCP servers.
|
|
20
23
|
- Turn-scoped MCP tools that are cleaned up after the turn.
|
|
24
|
+
- Opt-in Agent 365 chat-history submission for Microsoft's real-time threat
|
|
25
|
+
protection registration API.
|
|
21
26
|
- A Microsoft-specific adapter without importing Microsoft SDK types into
|
|
22
27
|
`agent-core`.
|
|
23
28
|
|
|
@@ -63,6 +68,69 @@ createA365ToolingTurnToolProvider({
|
|
|
63
68
|
});
|
|
64
69
|
```
|
|
65
70
|
|
|
71
|
+
## Chat History Submission
|
|
72
|
+
|
|
73
|
+
Microsoft's RTP API is developer-invoked; this package does not automatically
|
|
74
|
+
intercept every conversation. Register the middleware only when the host should
|
|
75
|
+
submit chat history for Microsoft Agent 365 analysis.
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
import { createAgent } from "@cuylabs/agent-core";
|
|
79
|
+
import {
|
|
80
|
+
createA365ChatHistoryMiddleware,
|
|
81
|
+
createA365ToolingTurnToolProvider,
|
|
82
|
+
} from "@cuylabs/agent-a365-tooling";
|
|
83
|
+
|
|
84
|
+
const agent = createAgent({
|
|
85
|
+
model,
|
|
86
|
+
turnToolProviders: [
|
|
87
|
+
createA365ToolingTurnToolProvider({
|
|
88
|
+
authorization,
|
|
89
|
+
authHandlerName: "agentic",
|
|
90
|
+
}),
|
|
91
|
+
],
|
|
92
|
+
middleware: [
|
|
93
|
+
createA365ChatHistoryMiddleware({
|
|
94
|
+
toolOptions: {
|
|
95
|
+
orchestratorName: "dory",
|
|
96
|
+
},
|
|
97
|
+
}),
|
|
98
|
+
],
|
|
99
|
+
});
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
The middleware defaults to `timing: "after-turn"`, `limit: 20`, and
|
|
103
|
+
`maxContentChars: 32000`. It reads history lazily from agent-core's lifecycle
|
|
104
|
+
context, converts text content to Microsoft's
|
|
105
|
+
`{ id, role, content, timestamp }` shape, keeps the most recent converted
|
|
106
|
+
messages within the content budget, and calls `sendChatHistory` even when the
|
|
107
|
+
converted history is `[]`. Empty arrays are intentional: Microsoft uses the
|
|
108
|
+
current `TurnContext.activity` as the message being registered, with
|
|
109
|
+
`chatHistory` as context.
|
|
110
|
+
On `onChatEnd`, agent-core resolves this lazy history from the completed turn
|
|
111
|
+
before any automatic context compaction rewrites the visible session history.
|
|
112
|
+
|
|
113
|
+
Tool messages are included by default to match Microsoft's standard-message
|
|
114
|
+
expectation. Set `includeTools: false` only when the host has a privacy or
|
|
115
|
+
compliance reason to filter tool outputs; doing so can reduce RTP context.
|
|
116
|
+
|
|
117
|
+
For non-M365 hosts, missing TurnContext is skipped by default:
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
createA365ChatHistoryMiddleware({
|
|
121
|
+
onMissingTurnContext: "skip", // "skip" | "warn"
|
|
122
|
+
getTurnContext: () => myTurnContext,
|
|
123
|
+
});
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
If another `onChatEnd` hook must run after the submission attempt, place it
|
|
127
|
+
later in the middleware array. The captured history leaf is fixed before
|
|
128
|
+
automatic compaction and before `onChatEnd` hooks run. The Microsoft SDK returns
|
|
129
|
+
transport success/failure only; threat verdicts are not returned inline through
|
|
130
|
+
`sendChatHistory`.
|
|
131
|
+
Submission failures are non-blocking by design; use `failureMode: "warn"` to
|
|
132
|
+
log them or `failureMode: "ignore"` to silence them.
|
|
133
|
+
|
|
66
134
|
## Concept Docs
|
|
67
135
|
|
|
68
136
|
The package docs are split by concern:
|
|
@@ -91,12 +159,25 @@ exchange for the active turn. In local dev-manifest mode, examples can pass a
|
|
|
91
159
|
fake decoded token because the SDK reads `ToolingManifest.json` instead of
|
|
92
160
|
calling the cloud gateway.
|
|
93
161
|
|
|
162
|
+
## Open Microsoft Questions
|
|
163
|
+
|
|
164
|
+
These do not block the v0 adapter, but they matter for production policy:
|
|
165
|
+
|
|
166
|
+
- Does `/agents/real-time-threat-protection/chat-message` require an
|
|
167
|
+
`Authorization` header in production? Microsoft's current SDK call path passes
|
|
168
|
+
`undefined` as the auth token.
|
|
169
|
+
- What is the server-side HTTP body size cap for `chatHistory`, and does
|
|
170
|
+
Microsoft recommend a client-side history limit?
|
|
171
|
+
- Are retries with the same `activity.id` / `messageId` guaranteed idempotent?
|
|
172
|
+
- Should post-turn `chatHistory` include the current user/assistant/tool
|
|
173
|
+
messages, or only messages before the current activity?
|
|
174
|
+
|
|
94
175
|
## What This Does Not Do
|
|
95
176
|
|
|
96
177
|
This package is not a channel, not an LLM provider, and not a replacement for
|
|
97
178
|
agent-core's MCP engine. It does not implement cross-turn MCP pooling,
|
|
98
|
-
mid-turn token refresh, or
|
|
99
|
-
|
|
179
|
+
mid-turn token refresh, or inline threat blocking. `sendChatHistory` is a
|
|
180
|
+
submit/register call; hosts should not treat it as a synchronous threat verdict.
|
|
100
181
|
|
|
101
182
|
## Examples
|
|
102
183
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import { AgentTurnToolContext, Logger, AgentTurnToolProviderResult, MCPServerStatus, AgentTurnToolProvider, MCPConfig } from '@cuylabs/agent-core';
|
|
1
|
+
import { ChatLifecycleContext, AgentTurnToolContext, Logger, AgentTurnToolProviderResult, MCPServerStatus, MessageRole, Message, AgentMiddleware, AgentTurnToolProvider, MCPConfig } from '@cuylabs/agent-core';
|
|
2
2
|
|
|
3
3
|
type MaybePromise<T> = T | Promise<T>;
|
|
4
4
|
type TurnMcpTools = NonNullable<AgentTurnToolProviderResult["mcpTools"]>;
|
|
5
5
|
type A365ToolingUtility = {
|
|
6
6
|
GetToolRequestHeaders?: (authToken?: string, turnContext?: A365TurnContextLike, options?: A365ToolingToolOptions) => Record<string, string>;
|
|
7
7
|
};
|
|
8
|
+
type A365ChatHistoryService = {
|
|
9
|
+
sendChatHistory(turnContext: A365TurnContextLike, chatHistoryMessages: A365ChatHistoryMessage[], options?: A365ToolingToolOptions): Promise<A365OperationResultLike>;
|
|
10
|
+
};
|
|
8
11
|
type A365TurnContextLike = {
|
|
9
12
|
activity?: unknown;
|
|
10
13
|
[key: string]: unknown;
|
|
@@ -26,6 +29,10 @@ interface A365ToolingProviderContext {
|
|
|
26
29
|
agentContext: AgentTurnToolContext;
|
|
27
30
|
turnContext: A365TurnContextLike;
|
|
28
31
|
}
|
|
32
|
+
interface A365ChatHistoryContext {
|
|
33
|
+
chatContext: ChatLifecycleContext;
|
|
34
|
+
turnContext: A365TurnContextLike;
|
|
35
|
+
}
|
|
29
36
|
interface A365ToolingConnectorContext extends A365ToolingProviderContext {
|
|
30
37
|
providerName: string;
|
|
31
38
|
serverTimeoutMs: number;
|
|
@@ -39,12 +46,128 @@ interface A365ToolingMcpConnection {
|
|
|
39
46
|
type A365ToolingMcpConnector = (servers: A365McpServerConfig[], context: A365ToolingConnectorContext) => Promise<A365ToolingMcpConnection>;
|
|
40
47
|
type A365ToolingServerFilter = (servers: A365McpServerConfig[], context: A365ToolingProviderContext) => MaybePromise<A365McpServerConfig[]>;
|
|
41
48
|
type A365ToolingValueResolver<T> = (context: A365ToolingProviderContext) => MaybePromise<T>;
|
|
49
|
+
type A365ChatHistoryValueResolver<T> = (context: A365ChatHistoryContext) => MaybePromise<T>;
|
|
50
|
+
interface A365ChatHistoryMessage {
|
|
51
|
+
id: string;
|
|
52
|
+
role: string;
|
|
53
|
+
content: string;
|
|
54
|
+
timestamp: string;
|
|
55
|
+
}
|
|
56
|
+
interface A365OperationResultLike {
|
|
57
|
+
succeeded: boolean;
|
|
58
|
+
errors?: readonly unknown[];
|
|
59
|
+
toString?: () => string;
|
|
60
|
+
}
|
|
42
61
|
interface A365ToolingModule {
|
|
43
62
|
Utility?: A365ToolingUtility;
|
|
44
63
|
McpToolServerConfigurationService: new (configurationProvider?: unknown) => {
|
|
45
64
|
listToolServers(turnContext: A365TurnContextLike, authorization: A365AuthorizationLike, authHandlerName: string, authToken?: string, options?: A365ToolingToolOptions): Promise<A365McpServerConfig[]>;
|
|
65
|
+
sendChatHistory?(turnContext: A365TurnContextLike, chatHistoryMessages: A365ChatHistoryMessage[], options?: A365ToolingToolOptions): Promise<A365OperationResultLike>;
|
|
46
66
|
};
|
|
47
67
|
}
|
|
68
|
+
type A365ChatHistoryFailureMode = "warn" | "ignore";
|
|
69
|
+
type A365MissingTurnContextMode = "skip" | "warn";
|
|
70
|
+
type A365ChatHistoryTiming = "after-turn";
|
|
71
|
+
interface ConvertAgentMessagesToA365ChatHistoryOptions {
|
|
72
|
+
/**
|
|
73
|
+
* Restrict converted messages to these roles. Defaults to all roles.
|
|
74
|
+
*/
|
|
75
|
+
roles?: readonly MessageRole[];
|
|
76
|
+
/**
|
|
77
|
+
* Include tool messages in the RTP context.
|
|
78
|
+
*
|
|
79
|
+
* Defaults to true to match Microsoft's PRD, which expects standard tool and
|
|
80
|
+
* function messages to be submitted unless the host explicitly filters them.
|
|
81
|
+
*/
|
|
82
|
+
includeTools?: boolean;
|
|
83
|
+
}
|
|
84
|
+
interface SendA365ChatHistoryOptions {
|
|
85
|
+
turnContext: A365TurnContextLike;
|
|
86
|
+
chatHistoryMessages: A365ChatHistoryMessage[];
|
|
87
|
+
toolOptions?: A365ToolingToolOptions;
|
|
88
|
+
configurationProvider?: unknown;
|
|
89
|
+
getToolingModule?: () => Promise<A365ToolingModule>;
|
|
90
|
+
serviceFactory?: (module: A365ToolingModule, configurationProvider: unknown) => A365ChatHistoryService;
|
|
91
|
+
}
|
|
92
|
+
interface CreateA365ChatHistoryMiddlewareOptions {
|
|
93
|
+
/**
|
|
94
|
+
* Middleware name used in agent-core middleware logs.
|
|
95
|
+
*
|
|
96
|
+
* @default "a365-chat-history"
|
|
97
|
+
*/
|
|
98
|
+
name?: string;
|
|
99
|
+
/**
|
|
100
|
+
* When to submit chat history. v0 supports post-turn submission only.
|
|
101
|
+
*
|
|
102
|
+
* @default "after-turn"
|
|
103
|
+
*/
|
|
104
|
+
timing?: A365ChatHistoryTiming;
|
|
105
|
+
/**
|
|
106
|
+
* Maximum recent messages to submit as context.
|
|
107
|
+
*
|
|
108
|
+
* Empty context is still submitted to register the current Microsoft
|
|
109
|
+
* activity with RTP.
|
|
110
|
+
*
|
|
111
|
+
* @default 20
|
|
112
|
+
*/
|
|
113
|
+
limit?: number;
|
|
114
|
+
/**
|
|
115
|
+
* Maximum combined characters from submitted chat history message content.
|
|
116
|
+
*
|
|
117
|
+
* The middleware keeps the most recent converted messages within this
|
|
118
|
+
* budget. If the oldest included message is too large, its content is
|
|
119
|
+
* truncated to fit. Empty context is still submitted when the budget is 0.
|
|
120
|
+
*
|
|
121
|
+
* @default 32000
|
|
122
|
+
*/
|
|
123
|
+
maxContentChars?: number;
|
|
124
|
+
/**
|
|
125
|
+
* Restrict submitted history to these roles. Defaults to all roles.
|
|
126
|
+
*/
|
|
127
|
+
roles?: readonly MessageRole[];
|
|
128
|
+
/**
|
|
129
|
+
* Include tool messages in the submitted history.
|
|
130
|
+
*
|
|
131
|
+
* @default true
|
|
132
|
+
*/
|
|
133
|
+
includeTools?: boolean;
|
|
134
|
+
/**
|
|
135
|
+
* Optional A365 tooling request options, such as `orchestratorName`.
|
|
136
|
+
*/
|
|
137
|
+
toolOptions?: A365ToolingToolOptions | A365ChatHistoryValueResolver<A365ToolingToolOptions | undefined>;
|
|
138
|
+
/**
|
|
139
|
+
* Gets the active Microsoft TurnContext.
|
|
140
|
+
*
|
|
141
|
+
* Defaults to reading `currentM365TurnContext()?.turnContext` from
|
|
142
|
+
* `@cuylabs/agent-channel-m365`. Non-M365 hosts are skipped by default.
|
|
143
|
+
*/
|
|
144
|
+
getTurnContext?: () => MaybePromise<A365TurnContextLike | undefined>;
|
|
145
|
+
/**
|
|
146
|
+
* What to do when no Microsoft TurnContext is available.
|
|
147
|
+
*
|
|
148
|
+
* @default "skip"
|
|
149
|
+
*/
|
|
150
|
+
onMissingTurnContext?: A365MissingTurnContextMode;
|
|
151
|
+
/**
|
|
152
|
+
* How to handle transport/module/conversion failures.
|
|
153
|
+
*
|
|
154
|
+
* @default "warn"
|
|
155
|
+
*/
|
|
156
|
+
failureMode?: A365ChatHistoryFailureMode;
|
|
157
|
+
/**
|
|
158
|
+
* Optional Microsoft A365 tooling configuration provider.
|
|
159
|
+
*/
|
|
160
|
+
configurationProvider?: unknown;
|
|
161
|
+
/**
|
|
162
|
+
* Test seam for loading `@microsoft/agents-a365-tooling`.
|
|
163
|
+
*/
|
|
164
|
+
getToolingModule?: () => Promise<A365ToolingModule>;
|
|
165
|
+
/**
|
|
166
|
+
* Advanced service factory override for tests or custom Microsoft SDK wiring.
|
|
167
|
+
*/
|
|
168
|
+
serviceFactory?: SendA365ChatHistoryOptions["serviceFactory"];
|
|
169
|
+
logger?: Logger;
|
|
170
|
+
}
|
|
48
171
|
interface CreateA365ToolingTurnToolProviderOptions {
|
|
49
172
|
/**
|
|
50
173
|
* Provider name used in duplicate-tool errors and logs.
|
|
@@ -126,7 +249,10 @@ declare class A365ToolingModuleLoadError extends Error {
|
|
|
126
249
|
constructor(packageName: string, cause: unknown);
|
|
127
250
|
}
|
|
128
251
|
declare function createA365ToolingTurnToolProvider(options: CreateA365ToolingTurnToolProviderOptions): AgentTurnToolProvider;
|
|
252
|
+
declare function convertAgentMessagesToA365ChatHistory(messages: readonly Message[], options?: ConvertAgentMessagesToA365ChatHistoryOptions): A365ChatHistoryMessage[];
|
|
253
|
+
declare function sendA365ChatHistory(options: SendA365ChatHistoryOptions): Promise<A365OperationResultLike>;
|
|
254
|
+
declare function createA365ChatHistoryMiddleware(options?: CreateA365ChatHistoryMiddlewareOptions): AgentMiddleware;
|
|
129
255
|
declare function connectA365McpServers(servers: A365McpServerConfig[], context: A365ToolingConnectorContext): Promise<A365ToolingMcpConnection>;
|
|
130
256
|
declare function toAgentCoreMcpConfig(servers: A365McpServerConfig[], options?: ToAgentCoreMcpConfigOptions): MCPConfig;
|
|
131
257
|
|
|
132
|
-
export { type A365AuthorizationLike, type A365McpServerConfig, type A365ToolingConnectorContext, type A365ToolingMcpConnection, type A365ToolingMcpConnector, type A365ToolingModule, A365ToolingModuleLoadError, type A365ToolingProviderContext, type A365ToolingServerFilter, type A365ToolingToolOptions, A365ToolingTurnContextError, type A365ToolingValueResolver, type A365TurnContextLike, type CreateA365ToolingTurnToolProviderOptions, type ToAgentCoreMcpConfigOptions, connectA365McpServers, createA365ToolingTurnToolProvider, toAgentCoreMcpConfig };
|
|
258
|
+
export { type A365AuthorizationLike, type A365ChatHistoryContext, type A365ChatHistoryFailureMode, type A365ChatHistoryMessage, type A365ChatHistoryTiming, type A365ChatHistoryValueResolver, type A365McpServerConfig, type A365MissingTurnContextMode, type A365OperationResultLike, type A365ToolingConnectorContext, type A365ToolingMcpConnection, type A365ToolingMcpConnector, type A365ToolingModule, A365ToolingModuleLoadError, type A365ToolingProviderContext, type A365ToolingServerFilter, type A365ToolingToolOptions, A365ToolingTurnContextError, type A365ToolingValueResolver, type A365TurnContextLike, type ConvertAgentMessagesToA365ChatHistoryOptions, type CreateA365ChatHistoryMiddlewareOptions, type CreateA365ToolingTurnToolProviderOptions, type SendA365ChatHistoryOptions, type ToAgentCoreMcpConfigOptions, connectA365McpServers, convertAgentMessagesToA365ChatHistory, createA365ChatHistoryMiddleware, createA365ToolingTurnToolProvider, sendA365ChatHistory, toAgentCoreMcpConfig };
|
package/dist/index.js
CHANGED
|
@@ -6,6 +6,15 @@ var A365_TOOLING_PACKAGE = "@microsoft/agents-a365-tooling";
|
|
|
6
6
|
var M365_CHANNEL_PACKAGE = "@cuylabs/agent-channel-m365";
|
|
7
7
|
var DEFAULT_PROVIDER_NAME = "a365-tooling";
|
|
8
8
|
var DEFAULT_SERVER_TIMEOUT_MS = 5e3;
|
|
9
|
+
var DEFAULT_CHAT_HISTORY_MIDDLEWARE_NAME = "a365-chat-history";
|
|
10
|
+
var DEFAULT_CHAT_HISTORY_LIMIT = 20;
|
|
11
|
+
var DEFAULT_CHAT_HISTORY_CONTENT_CHAR_LIMIT = 32e3;
|
|
12
|
+
var ALL_MESSAGE_ROLES = [
|
|
13
|
+
"system",
|
|
14
|
+
"user",
|
|
15
|
+
"assistant",
|
|
16
|
+
"tool"
|
|
17
|
+
];
|
|
9
18
|
var A365ToolingTurnContextError = class extends Error {
|
|
10
19
|
constructor(message = defaultMissingTurnContextMessage(), options) {
|
|
11
20
|
super(message, options);
|
|
@@ -92,6 +101,137 @@ function createA365ToolingTurnToolProvider(options) {
|
|
|
92
101
|
}
|
|
93
102
|
};
|
|
94
103
|
}
|
|
104
|
+
function convertAgentMessagesToA365ChatHistory(messages, options = {}) {
|
|
105
|
+
const roles = options.roles ? new Set(options.roles) : void 0;
|
|
106
|
+
const includeTools = options.includeTools ?? true;
|
|
107
|
+
const converted = [];
|
|
108
|
+
for (const message of messages) {
|
|
109
|
+
if (!includeTools && message.role === "tool") {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (roles && !roles.has(message.role)) {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const content = extractMessageContent(message);
|
|
116
|
+
if (content.trim().length === 0) {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
converted.push({
|
|
120
|
+
id: message.id,
|
|
121
|
+
role: message.role,
|
|
122
|
+
content,
|
|
123
|
+
timestamp: message.createdAt.toISOString()
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
return converted;
|
|
127
|
+
}
|
|
128
|
+
async function sendA365ChatHistory(options) {
|
|
129
|
+
const module = await loadToolingModule(options.getToolingModule);
|
|
130
|
+
const service = options.serviceFactory ? options.serviceFactory(module, options.configurationProvider) : new module.McpToolServerConfigurationService(
|
|
131
|
+
options.configurationProvider
|
|
132
|
+
);
|
|
133
|
+
if (typeof service.sendChatHistory !== "function") {
|
|
134
|
+
throw new Error(
|
|
135
|
+
"The loaded @microsoft/agents-a365-tooling service does not expose sendChatHistory."
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
return service.sendChatHistory(
|
|
139
|
+
options.turnContext,
|
|
140
|
+
options.chatHistoryMessages,
|
|
141
|
+
options.toolOptions
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
function createA365ChatHistoryMiddleware(options = {}) {
|
|
145
|
+
const name = normalizeRequiredString(
|
|
146
|
+
options.name ?? DEFAULT_CHAT_HISTORY_MIDDLEWARE_NAME,
|
|
147
|
+
"name"
|
|
148
|
+
);
|
|
149
|
+
const timing = options.timing ?? "after-turn";
|
|
150
|
+
if (timing !== "after-turn") {
|
|
151
|
+
throw new Error(
|
|
152
|
+
'A365 chat history middleware only supports timing "after-turn"'
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
const limit = normalizeChatHistoryLimit(options.limit);
|
|
156
|
+
const maxContentChars = normalizeChatHistoryContentCharLimit(
|
|
157
|
+
options.maxContentChars
|
|
158
|
+
);
|
|
159
|
+
const failureMode = normalizeChatHistoryFailureMode(options.failureMode);
|
|
160
|
+
const onMissingTurnContext = normalizeMissingTurnContextMode(
|
|
161
|
+
options.onMissingTurnContext
|
|
162
|
+
);
|
|
163
|
+
return {
|
|
164
|
+
name,
|
|
165
|
+
async onChatEnd(sessionId, _result, ctx) {
|
|
166
|
+
const chatContext = ctx ?? { sessionId };
|
|
167
|
+
let turnContext;
|
|
168
|
+
try {
|
|
169
|
+
turnContext = await resolveOptionalTurnContext(options.getTurnContext);
|
|
170
|
+
} catch (error) {
|
|
171
|
+
handleChatHistoryFailure(
|
|
172
|
+
failureMode,
|
|
173
|
+
options.logger,
|
|
174
|
+
name,
|
|
175
|
+
error,
|
|
176
|
+
chatContext
|
|
177
|
+
);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if (!turnContext) {
|
|
181
|
+
handleMissingChatHistoryTurnContext(
|
|
182
|
+
onMissingTurnContext,
|
|
183
|
+
options.logger,
|
|
184
|
+
name,
|
|
185
|
+
chatContext
|
|
186
|
+
);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
try {
|
|
190
|
+
const historyContext = {
|
|
191
|
+
chatContext,
|
|
192
|
+
turnContext
|
|
193
|
+
};
|
|
194
|
+
const toolOptions = await resolveChatHistoryOptionalValue(
|
|
195
|
+
options.toolOptions,
|
|
196
|
+
historyContext
|
|
197
|
+
);
|
|
198
|
+
const roles = resolveChatHistoryRoles(options);
|
|
199
|
+
const messages = chatContext.history?.getRecentMessages({ limit, roles }) ?? [];
|
|
200
|
+
const chatHistoryMessages = constrainA365ChatHistoryContent(
|
|
201
|
+
convertAgentMessagesToA365ChatHistory(messages, {
|
|
202
|
+
includeTools: options.includeTools ?? true
|
|
203
|
+
}),
|
|
204
|
+
maxContentChars
|
|
205
|
+
);
|
|
206
|
+
const operationResult = await sendA365ChatHistory({
|
|
207
|
+
turnContext,
|
|
208
|
+
chatHistoryMessages,
|
|
209
|
+
toolOptions,
|
|
210
|
+
configurationProvider: options.configurationProvider,
|
|
211
|
+
getToolingModule: options.getToolingModule,
|
|
212
|
+
serviceFactory: options.serviceFactory
|
|
213
|
+
});
|
|
214
|
+
if (!operationResult.succeeded) {
|
|
215
|
+
handleChatHistoryFailure(
|
|
216
|
+
failureMode,
|
|
217
|
+
options.logger,
|
|
218
|
+
name,
|
|
219
|
+
new Error(formatOperationResultError(operationResult)),
|
|
220
|
+
chatContext
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
} catch (error) {
|
|
224
|
+
handleChatHistoryFailure(
|
|
225
|
+
failureMode,
|
|
226
|
+
options.logger,
|
|
227
|
+
name,
|
|
228
|
+
error,
|
|
229
|
+
chatContext
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
}
|
|
95
235
|
async function connectA365McpServers(servers, context) {
|
|
96
236
|
const manager = createMCPManager(
|
|
97
237
|
toAgentCoreMcpConfig(servers, { timeoutMs: context.serverTimeoutMs })
|
|
@@ -175,6 +315,120 @@ function extractBearerToken(headers) {
|
|
|
175
315
|
}
|
|
176
316
|
return void 0;
|
|
177
317
|
}
|
|
318
|
+
function extractMessageContent(message) {
|
|
319
|
+
const rawMessage = message;
|
|
320
|
+
if (typeof rawMessage.text === "string" && rawMessage.text.trim()) {
|
|
321
|
+
return rawMessage.text;
|
|
322
|
+
}
|
|
323
|
+
const content = extractTextFromContent(rawMessage.content);
|
|
324
|
+
if (content.trim()) {
|
|
325
|
+
return content;
|
|
326
|
+
}
|
|
327
|
+
if (message.role === "assistant" && message.toolCalls?.length) {
|
|
328
|
+
return message.toolCalls.map(
|
|
329
|
+
(toolCall) => `tool_call: ${toolCall.toolName}(${stringifyToolArgs(toolCall.args)})`
|
|
330
|
+
).join("\n");
|
|
331
|
+
}
|
|
332
|
+
return typeof rawMessage.text === "string" ? rawMessage.text : "";
|
|
333
|
+
}
|
|
334
|
+
function stringifyToolArgs(args) {
|
|
335
|
+
try {
|
|
336
|
+
return JSON.stringify(args) ?? "undefined";
|
|
337
|
+
} catch {
|
|
338
|
+
return String(args);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
function extractTextFromContent(content) {
|
|
342
|
+
if (typeof content === "string") {
|
|
343
|
+
return content;
|
|
344
|
+
}
|
|
345
|
+
if (Array.isArray(content)) {
|
|
346
|
+
return content.map((part) => extractTextFromContentPart(part)).filter((part) => part.length > 0).join("\n");
|
|
347
|
+
}
|
|
348
|
+
if (content && typeof content === "object") {
|
|
349
|
+
const text = content.text;
|
|
350
|
+
if (typeof text === "string") {
|
|
351
|
+
return text;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return "";
|
|
355
|
+
}
|
|
356
|
+
function extractTextFromContentPart(part) {
|
|
357
|
+
if (typeof part === "string") {
|
|
358
|
+
return part;
|
|
359
|
+
}
|
|
360
|
+
if (!part || typeof part !== "object") {
|
|
361
|
+
return "";
|
|
362
|
+
}
|
|
363
|
+
const text = part.text;
|
|
364
|
+
if (typeof text === "string") {
|
|
365
|
+
return text;
|
|
366
|
+
}
|
|
367
|
+
const content = part.content;
|
|
368
|
+
return extractTextFromContent(content);
|
|
369
|
+
}
|
|
370
|
+
function resolveChatHistoryRoles(options) {
|
|
371
|
+
const roles = options.roles ?? ALL_MESSAGE_ROLES;
|
|
372
|
+
if (options.includeTools !== false) {
|
|
373
|
+
return roles;
|
|
374
|
+
}
|
|
375
|
+
return roles.filter((role) => role !== "tool");
|
|
376
|
+
}
|
|
377
|
+
function constrainA365ChatHistoryContent(messages, maxContentChars) {
|
|
378
|
+
if (messages.length === 0 || maxContentChars === 0) {
|
|
379
|
+
return [];
|
|
380
|
+
}
|
|
381
|
+
const constrained = [];
|
|
382
|
+
let remainingChars = maxContentChars;
|
|
383
|
+
for (let index = messages.length - 1; index >= 0; index--) {
|
|
384
|
+
const message = messages[index];
|
|
385
|
+
if (message.content.length <= remainingChars) {
|
|
386
|
+
constrained.push(message);
|
|
387
|
+
remainingChars -= message.content.length;
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
if (remainingChars > 0) {
|
|
391
|
+
const content = sliceWithoutDanglingSurrogate(
|
|
392
|
+
message.content,
|
|
393
|
+
remainingChars
|
|
394
|
+
);
|
|
395
|
+
constrained.push({
|
|
396
|
+
...message,
|
|
397
|
+
content
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
break;
|
|
401
|
+
}
|
|
402
|
+
constrained.reverse();
|
|
403
|
+
return constrained;
|
|
404
|
+
}
|
|
405
|
+
function sliceWithoutDanglingSurrogate(value, length) {
|
|
406
|
+
let sliced = value.slice(0, length);
|
|
407
|
+
const lastCode = sliced.charCodeAt(sliced.length - 1);
|
|
408
|
+
if (lastCode >= 55296 && lastCode <= 56319) {
|
|
409
|
+
sliced = sliced.slice(0, -1);
|
|
410
|
+
}
|
|
411
|
+
return sliced;
|
|
412
|
+
}
|
|
413
|
+
async function resolveChatHistoryOptionalValue(value, context) {
|
|
414
|
+
if (value === void 0) {
|
|
415
|
+
return void 0;
|
|
416
|
+
}
|
|
417
|
+
return typeof value === "function" ? value(context) : value;
|
|
418
|
+
}
|
|
419
|
+
async function resolveOptionalTurnContext(getTurnContext) {
|
|
420
|
+
if (getTurnContext) {
|
|
421
|
+
return getTurnContext();
|
|
422
|
+
}
|
|
423
|
+
try {
|
|
424
|
+
return await getDefaultM365TurnContext();
|
|
425
|
+
} catch (error) {
|
|
426
|
+
if (error instanceof A365ToolingTurnContextError) {
|
|
427
|
+
return void 0;
|
|
428
|
+
}
|
|
429
|
+
throw error;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
178
432
|
async function resolveTurnContext(getTurnContext) {
|
|
179
433
|
const turnContext = getTurnContext ? await getTurnContext() : await getDefaultM365TurnContext();
|
|
180
434
|
if (!turnContext) {
|
|
@@ -223,6 +477,34 @@ function normalizeTimeout(timeoutMs) {
|
|
|
223
477
|
}
|
|
224
478
|
return timeout;
|
|
225
479
|
}
|
|
480
|
+
function normalizeChatHistoryLimit(limit) {
|
|
481
|
+
const normalized = limit ?? DEFAULT_CHAT_HISTORY_LIMIT;
|
|
482
|
+
if (!Number.isFinite(normalized) || normalized < 0) {
|
|
483
|
+
throw new Error("limit must be a non-negative finite number");
|
|
484
|
+
}
|
|
485
|
+
return Math.floor(normalized);
|
|
486
|
+
}
|
|
487
|
+
function normalizeChatHistoryContentCharLimit(maxContentChars) {
|
|
488
|
+
const normalized = maxContentChars ?? DEFAULT_CHAT_HISTORY_CONTENT_CHAR_LIMIT;
|
|
489
|
+
if (!Number.isFinite(normalized) || normalized < 0) {
|
|
490
|
+
throw new Error("maxContentChars must be a non-negative finite number");
|
|
491
|
+
}
|
|
492
|
+
return Math.floor(normalized);
|
|
493
|
+
}
|
|
494
|
+
function normalizeChatHistoryFailureMode(mode) {
|
|
495
|
+
const normalized = mode ?? "warn";
|
|
496
|
+
if (normalized !== "warn" && normalized !== "ignore") {
|
|
497
|
+
throw new Error('failureMode must be "warn" or "ignore"');
|
|
498
|
+
}
|
|
499
|
+
return normalized;
|
|
500
|
+
}
|
|
501
|
+
function normalizeMissingTurnContextMode(mode) {
|
|
502
|
+
const normalized = mode ?? "skip";
|
|
503
|
+
if (normalized !== "skip" && normalized !== "warn") {
|
|
504
|
+
throw new Error('onMissingTurnContext must be "skip" or "warn"');
|
|
505
|
+
}
|
|
506
|
+
return normalized;
|
|
507
|
+
}
|
|
226
508
|
function normalizeRequiredString(value, name) {
|
|
227
509
|
const trimmed = value?.trim();
|
|
228
510
|
if (!trimmed) {
|
|
@@ -230,6 +512,46 @@ function normalizeRequiredString(value, name) {
|
|
|
230
512
|
}
|
|
231
513
|
return trimmed;
|
|
232
514
|
}
|
|
515
|
+
function handleMissingChatHistoryTurnContext(mode, logger, middlewareName, ctx) {
|
|
516
|
+
const message = "A365 chat history submission skipped: no ambient Microsoft TurnContext";
|
|
517
|
+
if (mode === "warn") {
|
|
518
|
+
logger?.warn(message, {
|
|
519
|
+
middleware: middlewareName,
|
|
520
|
+
sessionId: ctx.sessionId,
|
|
521
|
+
turnId: ctx.turnId
|
|
522
|
+
});
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
logger?.debug(message, {
|
|
526
|
+
middleware: middlewareName,
|
|
527
|
+
sessionId: ctx.sessionId,
|
|
528
|
+
turnId: ctx.turnId
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
function handleChatHistoryFailure(mode, logger, middlewareName, error, ctx) {
|
|
532
|
+
if (mode === "warn") {
|
|
533
|
+
const errorInstance = error instanceof Error ? error : new Error(String(error));
|
|
534
|
+
logger?.warn("A365 chat history submission failed", {
|
|
535
|
+
middleware: middlewareName,
|
|
536
|
+
sessionId: ctx.sessionId,
|
|
537
|
+
turnId: ctx.turnId,
|
|
538
|
+
error: errorInstance,
|
|
539
|
+
message: errorInstance.message,
|
|
540
|
+
stack: errorInstance.stack
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
function formatOperationResultError(result) {
|
|
545
|
+
const errors = Array.from(result.errors ?? []);
|
|
546
|
+
if (errors.length > 0) {
|
|
547
|
+
return errors.map((error) => error instanceof Error ? error.message : String(error)).join("; ");
|
|
548
|
+
}
|
|
549
|
+
const formatted = result.toString?.();
|
|
550
|
+
if (formatted) {
|
|
551
|
+
return formatted;
|
|
552
|
+
}
|
|
553
|
+
return "Microsoft A365 sendChatHistory returned an unsuccessful result";
|
|
554
|
+
}
|
|
233
555
|
function logConnectionStatuses(logger, providerName, statuses) {
|
|
234
556
|
if (!logger || !statuses) {
|
|
235
557
|
return;
|
|
@@ -261,6 +583,9 @@ export {
|
|
|
261
583
|
A365ToolingModuleLoadError,
|
|
262
584
|
A365ToolingTurnContextError,
|
|
263
585
|
connectA365McpServers,
|
|
586
|
+
convertAgentMessagesToA365ChatHistory,
|
|
587
|
+
createA365ChatHistoryMiddleware,
|
|
264
588
|
createA365ToolingTurnToolProvider,
|
|
589
|
+
sendA365ChatHistory,
|
|
265
590
|
toAgentCoreMcpConfig
|
|
266
591
|
};
|
|
@@ -82,6 +82,17 @@ There is no package-level close timeout in v0. If production deployments show
|
|
|
82
82
|
tail-latency spikes during turn cleanup, add a future `closeTimeoutMs` option to
|
|
83
83
|
the connector path.
|
|
84
84
|
|
|
85
|
+
## Chat History Submission
|
|
86
|
+
|
|
87
|
+
`createA365ChatHistoryMiddleware()` is opt-in and runs after a completed
|
|
88
|
+
agent-core turn. It reads recent messages through the lazy lifecycle history
|
|
89
|
+
accessor, converts them to Microsoft's chat-history shape, and calls
|
|
90
|
+
`sendChatHistory`.
|
|
91
|
+
|
|
92
|
+
The middleware submits transport registration only. It does not turn the RTP
|
|
93
|
+
call into a synchronous blocking verdict, and failures are non-blocking by
|
|
94
|
+
default.
|
|
95
|
+
|
|
85
96
|
## Abort
|
|
86
97
|
|
|
87
98
|
`AgentTurnToolContext.abort` is passed to providers, but agent-core's MCP
|
|
@@ -104,7 +115,7 @@ v0 does not include:
|
|
|
104
115
|
|
|
105
116
|
- cross-turn MCP client pooling;
|
|
106
117
|
- mid-turn token refresh;
|
|
107
|
-
- real-time threat protection
|
|
118
|
+
- inline real-time threat protection blocking;
|
|
108
119
|
- generic platform-neutral MCP discovery abstraction;
|
|
109
120
|
- dedicated cleanup timeout;
|
|
110
121
|
- custom telemetry events for per-server partial failure.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cuylabs/agent-a365-tooling",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
4
|
"description": "Microsoft Agent 365 tooling adapter for @cuylabs/agent-core turn-scoped MCP tools",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -18,12 +18,12 @@
|
|
|
18
18
|
"README.md"
|
|
19
19
|
],
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@cuylabs/agent-core": "^
|
|
21
|
+
"@cuylabs/agent-core": "^4.0.0"
|
|
22
22
|
},
|
|
23
23
|
"peerDependencies": {
|
|
24
24
|
"@microsoft/agents-a365-tooling": ">=0.2.0-preview.5 <1.0.0",
|
|
25
25
|
"@microsoft/agents-hosting": ">=1.4.0",
|
|
26
|
-
"@cuylabs/agent-channel-m365": "^
|
|
26
|
+
"@cuylabs/agent-channel-m365": "^4.0.0"
|
|
27
27
|
},
|
|
28
28
|
"peerDependenciesMeta": {
|
|
29
29
|
"@cuylabs/agent-channel-m365": {
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"typescript": "^5.7.0",
|
|
44
44
|
"vitest": "^4.0.18",
|
|
45
45
|
"zod": "^3.25.76 || ^4.1.8",
|
|
46
|
-
"@cuylabs/agent-channel-m365": "^
|
|
46
|
+
"@cuylabs/agent-channel-m365": "^4.0.0"
|
|
47
47
|
},
|
|
48
48
|
"keywords": [
|
|
49
49
|
"agent",
|