@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 CHANGED
@@ -2,10 +2,13 @@
2
2
 
3
3
  Microsoft Agent 365 tooling adapter for `@cuylabs/agent-core`.
4
4
 
5
- This package exposes one turn-scoped tool provider factory. On each chat turn,
6
- it uses Microsoft's Agent 365 tooling SDK to discover MCP servers for the active
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 Agent 365 real-time threat protection chat-history
99
- submission in v0.
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 chat-history submission;
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.1.0",
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": "^3.1.0"
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": "^3.1.0"
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": "^3.1.0"
46
+ "@cuylabs/agent-channel-m365": "^4.0.0"
47
47
  },
48
48
  "keywords": [
49
49
  "agent",