@ouro.bot/cli 0.1.0-alpha.14 → 0.1.0-alpha.16
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/heart/config.js +29 -1
- package/dist/heart/core.js +63 -0
- package/dist/heart/daemon/daemon-cli.js +2 -1
- package/dist/repertoire/ado-client.js +4 -2
- package/dist/repertoire/data/ado-endpoints.json +188 -0
- package/dist/repertoire/tools-teams.js +57 -4
- package/dist/repertoire/tools.js +11 -1
- package/dist/senses/teams.js +173 -54
- package/package.json +1 -1
package/dist/heart/config.js
CHANGED
|
@@ -41,8 +41,10 @@ exports.getMinimaxConfig = getMinimaxConfig;
|
|
|
41
41
|
exports.getAnthropicConfig = getAnthropicConfig;
|
|
42
42
|
exports.getOpenAICodexConfig = getOpenAICodexConfig;
|
|
43
43
|
exports.getTeamsConfig = getTeamsConfig;
|
|
44
|
+
exports.getTeamsSecondaryConfig = getTeamsSecondaryConfig;
|
|
44
45
|
exports.getContextConfig = getContextConfig;
|
|
45
46
|
exports.getOAuthConfig = getOAuthConfig;
|
|
47
|
+
exports.resolveOAuthForTenant = resolveOAuthForTenant;
|
|
46
48
|
exports.getTeamsChannelConfig = getTeamsChannelConfig;
|
|
47
49
|
exports.getBlueBubblesConfig = getBlueBubblesConfig;
|
|
48
50
|
exports.getBlueBubblesChannelConfig = getBlueBubblesChannelConfig;
|
|
@@ -84,12 +86,19 @@ const DEFAULT_SECRETS_TEMPLATE = {
|
|
|
84
86
|
clientId: "",
|
|
85
87
|
clientSecret: "",
|
|
86
88
|
tenantId: "",
|
|
89
|
+
managedIdentityClientId: "",
|
|
87
90
|
},
|
|
88
91
|
oauth: {
|
|
89
92
|
graphConnectionName: "graph",
|
|
90
93
|
adoConnectionName: "ado",
|
|
91
94
|
githubConnectionName: "",
|
|
92
95
|
},
|
|
96
|
+
teamsSecondary: {
|
|
97
|
+
clientId: "",
|
|
98
|
+
clientSecret: "",
|
|
99
|
+
tenantId: "",
|
|
100
|
+
managedIdentityClientId: "",
|
|
101
|
+
},
|
|
93
102
|
teamsChannel: {
|
|
94
103
|
skipConfirmation: true,
|
|
95
104
|
port: 3978,
|
|
@@ -118,6 +127,7 @@ function defaultRuntimeConfig() {
|
|
|
118
127
|
"openai-codex": { ...DEFAULT_SECRETS_TEMPLATE.providers["openai-codex"] },
|
|
119
128
|
},
|
|
120
129
|
teams: { ...DEFAULT_SECRETS_TEMPLATE.teams },
|
|
130
|
+
teamsSecondary: { ...DEFAULT_SECRETS_TEMPLATE.teamsSecondary },
|
|
121
131
|
oauth: { ...DEFAULT_SECRETS_TEMPLATE.oauth },
|
|
122
132
|
context: { ...identity_1.DEFAULT_AGENT_CONTEXT },
|
|
123
133
|
teamsChannel: { ...DEFAULT_SECRETS_TEMPLATE.teamsChannel },
|
|
@@ -262,6 +272,10 @@ function getTeamsConfig() {
|
|
|
262
272
|
const config = loadConfig();
|
|
263
273
|
return { ...config.teams };
|
|
264
274
|
}
|
|
275
|
+
function getTeamsSecondaryConfig() {
|
|
276
|
+
const config = loadConfig();
|
|
277
|
+
return { ...config.teamsSecondary };
|
|
278
|
+
}
|
|
265
279
|
function getContextConfig() {
|
|
266
280
|
if (_testContextOverride) {
|
|
267
281
|
return { ..._testContextOverride };
|
|
@@ -282,6 +296,16 @@ function getOAuthConfig() {
|
|
|
282
296
|
const config = loadConfig();
|
|
283
297
|
return { ...config.oauth };
|
|
284
298
|
}
|
|
299
|
+
/** Resolve OAuth connection names for a specific tenant, falling back to defaults. */
|
|
300
|
+
function resolveOAuthForTenant(tenantId) {
|
|
301
|
+
const base = getOAuthConfig();
|
|
302
|
+
const overrides = tenantId ? base.tenantOverrides?.[tenantId] : undefined;
|
|
303
|
+
return {
|
|
304
|
+
graphConnectionName: overrides?.graphConnectionName ?? base.graphConnectionName,
|
|
305
|
+
adoConnectionName: overrides?.adoConnectionName ?? base.adoConnectionName,
|
|
306
|
+
githubConnectionName: overrides?.githubConnectionName ?? base.githubConnectionName,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
285
309
|
function getTeamsChannelConfig() {
|
|
286
310
|
const config = loadConfig();
|
|
287
311
|
const { skipConfirmation, flushIntervalMs, port } = config.teamsChannel;
|
|
@@ -321,7 +345,11 @@ function sanitizeKey(key) {
|
|
|
321
345
|
return key.replace(/[/:]/g, "_");
|
|
322
346
|
}
|
|
323
347
|
function sessionPath(friendId, channel, key) {
|
|
324
|
-
|
|
348
|
+
// On Azure App Service, os.homedir() returns /root which is ephemeral.
|
|
349
|
+
// Use /home (persistent storage) when WEBSITE_SITE_NAME is set.
|
|
350
|
+
/* v8 ignore next -- Azure vs local path branch; environment-specific @preserve */
|
|
351
|
+
const homeBase = process.env.WEBSITE_SITE_NAME ? "/home" : os.homedir();
|
|
352
|
+
const dir = path.join(homeBase, ".agentstate", (0, identity_1.getAgentName)(), "sessions", friendId, channel);
|
|
325
353
|
fs.mkdirSync(dir, { recursive: true });
|
|
326
354
|
return path.join(dir, sanitizeKey(key) + ".json");
|
|
327
355
|
}
|
package/dist/heart/core.js
CHANGED
|
@@ -8,6 +8,7 @@ exports.getProvider = getProvider;
|
|
|
8
8
|
exports.createSummarize = createSummarize;
|
|
9
9
|
exports.getProviderDisplayLabel = getProviderDisplayLabel;
|
|
10
10
|
exports.stripLastToolCalls = stripLastToolCalls;
|
|
11
|
+
exports.repairOrphanedToolCalls = repairOrphanedToolCalls;
|
|
11
12
|
exports.isTransientError = isTransientError;
|
|
12
13
|
exports.classifyTransientError = classifyTransientError;
|
|
13
14
|
exports.runAgent = runAgent;
|
|
@@ -160,6 +161,68 @@ function stripLastToolCalls(messages) {
|
|
|
160
161
|
}
|
|
161
162
|
}
|
|
162
163
|
}
|
|
164
|
+
// Roles that end a tool-result scan. When scanning forward from an assistant
|
|
165
|
+
// message, stop at the next assistant or user message (tool results must be
|
|
166
|
+
// adjacent to their originating assistant message).
|
|
167
|
+
const TOOL_SCAN_BOUNDARY_ROLES = new Set(["assistant", "user"]);
|
|
168
|
+
// Repair orphaned tool_calls and tool results anywhere in the message history.
|
|
169
|
+
// 1. If an assistant message has tool_calls but missing tool results, inject synthetic error results.
|
|
170
|
+
// 2. If a tool result's tool_call_id doesn't match any tool_calls in a preceding assistant message, remove it.
|
|
171
|
+
// This prevents 400 errors from the API after an aborted turn.
|
|
172
|
+
function repairOrphanedToolCalls(messages) {
|
|
173
|
+
// Pass 1: collect all valid tool_call IDs from assistant messages
|
|
174
|
+
const validCallIds = new Set();
|
|
175
|
+
for (const msg of messages) {
|
|
176
|
+
if (msg.role === "assistant") {
|
|
177
|
+
const asst = msg;
|
|
178
|
+
if (asst.tool_calls) {
|
|
179
|
+
for (const tc of asst.tool_calls)
|
|
180
|
+
validCallIds.add(tc.id);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
// Pass 2: remove orphaned tool results (tool_call_id not in any assistant's tool_calls)
|
|
185
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
186
|
+
if (messages[i].role === "tool") {
|
|
187
|
+
const toolMsg = messages[i];
|
|
188
|
+
if (!validCallIds.has(toolMsg.tool_call_id)) {
|
|
189
|
+
messages.splice(i, 1);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// Pass 3: inject synthetic results for tool_calls missing their tool results
|
|
194
|
+
for (let i = 0; i < messages.length; i++) {
|
|
195
|
+
const msg = messages[i];
|
|
196
|
+
if (msg.role !== "assistant")
|
|
197
|
+
continue;
|
|
198
|
+
const asst = msg;
|
|
199
|
+
if (!asst.tool_calls || asst.tool_calls.length === 0)
|
|
200
|
+
continue;
|
|
201
|
+
// Collect tool result IDs that follow this assistant message
|
|
202
|
+
const resultIds = new Set();
|
|
203
|
+
for (let j = i + 1; j < messages.length; j++) {
|
|
204
|
+
const following = messages[j];
|
|
205
|
+
if (following.role === "tool") {
|
|
206
|
+
resultIds.add(following.tool_call_id);
|
|
207
|
+
}
|
|
208
|
+
else if (TOOL_SCAN_BOUNDARY_ROLES.has(following.role)) {
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
const missing = asst.tool_calls.filter((tc) => !resultIds.has(tc.id));
|
|
213
|
+
if (missing.length > 0) {
|
|
214
|
+
const syntheticResults = missing.map((tc) => ({
|
|
215
|
+
role: "tool",
|
|
216
|
+
tool_call_id: tc.id,
|
|
217
|
+
content: "error: tool call was interrupted (previous turn timed out or was aborted)",
|
|
218
|
+
}));
|
|
219
|
+
let insertAt = i + 1;
|
|
220
|
+
while (insertAt < messages.length && messages[insertAt].role === "tool")
|
|
221
|
+
insertAt++;
|
|
222
|
+
messages.splice(insertAt, 0, ...syntheticResults);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
163
226
|
// Detect context overflow errors from Azure or MiniMax
|
|
164
227
|
function isContextOverflow(err) {
|
|
165
228
|
if (!(err instanceof Error))
|
|
@@ -571,7 +571,8 @@ function defaultListDiscoveredAgents() {
|
|
|
571
571
|
return discovered.sort((left, right) => left.localeCompare(right));
|
|
572
572
|
}
|
|
573
573
|
async function defaultLinkFriendIdentity(command) {
|
|
574
|
-
const
|
|
574
|
+
const fp = path.join((0, identity_1.getAgentBundlesRoot)(), `${command.agent}.ouro`, "friends");
|
|
575
|
+
const friendStore = new store_file_1.FileFriendStore(fp);
|
|
575
576
|
const current = await friendStore.get(command.friendId);
|
|
576
577
|
if (!current) {
|
|
577
578
|
return `friend not found: ${command.friendId}`;
|
|
@@ -28,8 +28,10 @@ function resolveContentType(method, path) {
|
|
|
28
28
|
: "application/json";
|
|
29
29
|
}
|
|
30
30
|
// Generic ADO API request. Returns response body as pretty-printed JSON string.
|
|
31
|
-
|
|
31
|
+
// `host` overrides the base URL for non-standard APIs (e.g. "vsapm.dev.azure.com", "vssps.dev.azure.com").
|
|
32
|
+
async function adoRequest(token, method, org, path, body, host) {
|
|
32
33
|
try {
|
|
34
|
+
const base = host ? `https://${host}/${org}` : `${ADO_BASE}/${org}`;
|
|
33
35
|
(0, runtime_1.emitNervesEvent)({
|
|
34
36
|
event: "client.request_start",
|
|
35
37
|
component: "clients",
|
|
@@ -37,7 +39,7 @@ async function adoRequest(token, method, org, path, body) {
|
|
|
37
39
|
meta: { client: "ado", method, org, path },
|
|
38
40
|
});
|
|
39
41
|
const fullPath = ensureApiVersion(path);
|
|
40
|
-
const url = `${
|
|
42
|
+
const url = `${base}${fullPath}`;
|
|
41
43
|
const contentType = resolveContentType(method, path);
|
|
42
44
|
const opts = {
|
|
43
45
|
method,
|
|
@@ -35,6 +35,12 @@
|
|
|
35
35
|
"description": "Delete a work item (moves to recycle bin)",
|
|
36
36
|
"params": "destroy (boolean, permanently delete)"
|
|
37
37
|
},
|
|
38
|
+
{
|
|
39
|
+
"path": "/{project}/_apis/wit/workitemtypes",
|
|
40
|
+
"method": "GET",
|
|
41
|
+
"description": "List all work item types available in a project (Bug, Task, Epic, User Story, etc.)",
|
|
42
|
+
"params": ""
|
|
43
|
+
},
|
|
38
44
|
{
|
|
39
45
|
"path": "/_apis/git/repositories",
|
|
40
46
|
"method": "GET",
|
|
@@ -118,5 +124,187 @@
|
|
|
118
124
|
"method": "GET",
|
|
119
125
|
"description": "List saved work item queries (shared and personal)",
|
|
120
126
|
"params": "$depth, $expand"
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
"path": "/_apis/groupentitlements?api-version=7.1",
|
|
130
|
+
"method": "GET",
|
|
131
|
+
"host": "vsaex.dev.azure.com",
|
|
132
|
+
"description": "List group entitlements (group rules that auto-assign licenses). Use host vsaex.dev.azure.com.",
|
|
133
|
+
"params": ""
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
"path": "/_apis/groupentitlements?api-version=7.1",
|
|
137
|
+
"method": "POST",
|
|
138
|
+
"host": "vsaex.dev.azure.com",
|
|
139
|
+
"description": "Create a group entitlement rule — maps an AAD group to an access level (e.g. Basic) and project membership. All members of the AAD group automatically get the specified license. Use host vsaex.dev.azure.com. This is the best way to bulk-provision users.",
|
|
140
|
+
"params": "body: { group: { origin: 'aad', originId: '<AAD-group-object-id>', subjectKind: 'group' }, licenseRule: { licensingSource: 'account', accountLicenseType: 'express', licenseDisplayName: 'Basic' }, projectEntitlements: [{ group: { groupType: 'projectContributor' }, projectRef: { id: '<project-id>' } }] }"
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
"path": "/_apis/groupentitlements/{groupId}?api-version=7.1",
|
|
144
|
+
"method": "GET",
|
|
145
|
+
"host": "vsaex.dev.azure.com",
|
|
146
|
+
"description": "Get a specific group entitlement by ID. Use host vsaex.dev.azure.com.",
|
|
147
|
+
"params": ""
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
"path": "/_apis/groupentitlements/{groupId}?api-version=7.1",
|
|
151
|
+
"method": "PATCH",
|
|
152
|
+
"host": "vsaex.dev.azure.com",
|
|
153
|
+
"description": "Update a group entitlement (change license rule, project access). Use host vsaex.dev.azure.com.",
|
|
154
|
+
"params": "JSON Patch array: [{op, path, value}]"
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
"path": "/_apis/groupentitlements/{groupId}?api-version=7.1",
|
|
158
|
+
"method": "DELETE",
|
|
159
|
+
"host": "vsaex.dev.azure.com",
|
|
160
|
+
"description": "Delete a group entitlement rule. Use host vsaex.dev.azure.com.",
|
|
161
|
+
"params": ""
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
"path": "/_apis/memberentitlementmanagement/memberentitlements?api-version=7.1-preview.3",
|
|
165
|
+
"method": "GET",
|
|
166
|
+
"host": "vsapm.dev.azure.com",
|
|
167
|
+
"description": "List individual member entitlements (users and their access levels). Use host vsapm.dev.azure.com. For bulk provisioning, prefer the Group Entitlements API on vsaex.dev.azure.com instead.",
|
|
168
|
+
"params": "$top, $skip, $filter, $orderBy, $select"
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
"path": "/_apis/memberentitlementmanagement/memberentitlements?api-version=7.1-preview.3",
|
|
172
|
+
"method": "POST",
|
|
173
|
+
"host": "vsapm.dev.azure.com",
|
|
174
|
+
"description": "Add a single member entitlement. Use host vsapm.dev.azure.com. For bulk provisioning, prefer the Group Entitlements API on vsaex.dev.azure.com instead.",
|
|
175
|
+
"params": "body: { accessLevel: { accountLicenseType: 'express'|'stakeholder', licensingSource: 'account' }, user: { principalName: 'user@domain.com', subjectKind: 'user' }, projectEntitlements: [{ group: { groupType: 'projectContributor' }, projectRef: { id: projectId } }] }"
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
"path": "/_apis/memberentitlementmanagement/memberentitlements/{memberId}?api-version=7.1-preview.3",
|
|
179
|
+
"method": "PATCH",
|
|
180
|
+
"host": "vsapm.dev.azure.com",
|
|
181
|
+
"description": "Update a member entitlement (change access level, project access). Use host vsapm.dev.azure.com.",
|
|
182
|
+
"params": "JSON Patch array: [{op, path, value}]"
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
"path": "/_apis/memberentitlementmanagement/memberentitlements/{memberId}?api-version=7.1-preview.3",
|
|
186
|
+
"method": "DELETE",
|
|
187
|
+
"host": "vsapm.dev.azure.com",
|
|
188
|
+
"description": "Remove a member entitlement (revoke user access). Use host vsapm.dev.azure.com.",
|
|
189
|
+
"params": ""
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
"path": "/_apis/graph/users?api-version=7.1-preview.1",
|
|
193
|
+
"method": "GET",
|
|
194
|
+
"host": "vssps.dev.azure.com",
|
|
195
|
+
"description": "List users in the organization (Graph API). Use host vssps.dev.azure.com. IMPORTANT: include the full path with api-version as shown.",
|
|
196
|
+
"params": "subjectTypes (aad, msa, etc.), continuationToken"
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
"path": "/_apis/graph/groups?api-version=7.1-preview.1",
|
|
200
|
+
"method": "GET",
|
|
201
|
+
"host": "vssps.dev.azure.com",
|
|
202
|
+
"description": "List groups in the organization. Use host vssps.dev.azure.com. IMPORTANT: include the full path with api-version as shown.",
|
|
203
|
+
"params": "subjectTypes, continuationToken"
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
"path": "/_apis/graph/memberships/{subjectDescriptor}?api-version=7.1-preview.1",
|
|
207
|
+
"method": "GET",
|
|
208
|
+
"host": "vssps.dev.azure.com",
|
|
209
|
+
"description": "List group memberships for a user or group. Use host vssps.dev.azure.com. IMPORTANT: include the full path with api-version as shown.",
|
|
210
|
+
"params": "direction (up = groups user belongs to, down = members of group)"
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
"path": "/_apis/graph/memberships/{subjectDescriptor}/{containerDescriptor}?api-version=7.1-preview.1",
|
|
214
|
+
"method": "PUT",
|
|
215
|
+
"host": "vssps.dev.azure.com",
|
|
216
|
+
"description": "Add a user to a group. Use host vssps.dev.azure.com. IMPORTANT: include the full path with api-version as shown.",
|
|
217
|
+
"params": ""
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
"path": "/_apis/graph/memberships/{subjectDescriptor}/{containerDescriptor}?api-version=7.1-preview.1",
|
|
221
|
+
"method": "DELETE",
|
|
222
|
+
"host": "vssps.dev.azure.com",
|
|
223
|
+
"description": "Remove a user from a group. Use host vssps.dev.azure.com. IMPORTANT: include the full path with api-version as shown.",
|
|
224
|
+
"params": ""
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
"path": "/_apis/projects/{projectId}/teams",
|
|
228
|
+
"method": "GET",
|
|
229
|
+
"description": "List teams in a project",
|
|
230
|
+
"params": "$top, $skip"
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
"path": "/_apis/projects/{projectId}/teams/{teamId}",
|
|
234
|
+
"method": "GET",
|
|
235
|
+
"description": "Get a specific team by ID",
|
|
236
|
+
"params": ""
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
"path": "/_apis/projects/{projectId}/teams",
|
|
240
|
+
"method": "POST",
|
|
241
|
+
"description": "Create a new team in a project",
|
|
242
|
+
"params": "name, description"
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
"path": "/_apis/projects/{projectId}/teams/{teamId}/members",
|
|
246
|
+
"method": "GET",
|
|
247
|
+
"description": "List members of a team",
|
|
248
|
+
"params": "$top, $skip"
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
"path": "/{project}/{team}/_apis/work/teamsettings/iterations",
|
|
252
|
+
"method": "GET",
|
|
253
|
+
"description": "List iterations (sprints) for a team",
|
|
254
|
+
"params": "$timeframe (current, past, future)"
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
"path": "/{project}/{team}/_apis/work/teamsettings/iterations",
|
|
258
|
+
"method": "POST",
|
|
259
|
+
"description": "Add an iteration to a team's sprint schedule",
|
|
260
|
+
"params": "id (iteration node ID)"
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
"path": "/{project}/{team}/_apis/work/teamsettings/iterations/{iterationId}",
|
|
264
|
+
"method": "DELETE",
|
|
265
|
+
"description": "Remove an iteration from a team's sprint schedule",
|
|
266
|
+
"params": ""
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
"path": "/{project}/_apis/wit/classificationnodes/iterations",
|
|
270
|
+
"method": "GET",
|
|
271
|
+
"description": "List iteration path tree (project-level iteration nodes)",
|
|
272
|
+
"params": "$depth"
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
"path": "/{project}/_apis/wit/classificationnodes/iterations",
|
|
276
|
+
"method": "POST",
|
|
277
|
+
"description": "Create a new iteration node (sprint)",
|
|
278
|
+
"params": "name, attributes: { startDate, finishDate }"
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
"path": "/{project}/_apis/wit/classificationnodes/areas",
|
|
282
|
+
"method": "GET",
|
|
283
|
+
"description": "List area path tree (project-level area nodes)",
|
|
284
|
+
"params": "$depth"
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
"path": "/{project}/_apis/wit/classificationnodes/areas",
|
|
288
|
+
"method": "POST",
|
|
289
|
+
"description": "Create a new area path node",
|
|
290
|
+
"params": "name"
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
"path": "/{project}/_apis/wit/classificationnodes/{structureGroup}/{path}",
|
|
294
|
+
"method": "DELETE",
|
|
295
|
+
"description": "Delete a classification node (area or iteration). structureGroup is 'areas' or 'iterations'.",
|
|
296
|
+
"params": "$reclassifyId (move items to this node before deleting)"
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
"path": "/_apis/hooks/subscriptions",
|
|
300
|
+
"method": "GET",
|
|
301
|
+
"description": "List service hook subscriptions (webhooks for events)",
|
|
302
|
+
"params": ""
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
"path": "/_apis/hooks/subscriptions",
|
|
306
|
+
"method": "POST",
|
|
307
|
+
"description": "Create a service hook subscription (webhook)",
|
|
308
|
+
"params": "publisherId, eventType, consumerId, consumerActionId, publisherInputs, consumerInputs"
|
|
121
309
|
}
|
|
122
310
|
]
|
|
@@ -82,7 +82,7 @@ exports.teamsToolDefinitions = [
|
|
|
82
82
|
type: "function",
|
|
83
83
|
function: {
|
|
84
84
|
name: "ado_query",
|
|
85
|
-
description: "GET or POST (for WIQL read queries) any Azure DevOps API endpoint. Use ado_docs first to look up the correct path.",
|
|
85
|
+
description: "GET or POST (for WIQL read queries) any Azure DevOps API endpoint. Use ado_docs first to look up the correct path and host.",
|
|
86
86
|
parameters: {
|
|
87
87
|
type: "object",
|
|
88
88
|
properties: {
|
|
@@ -90,6 +90,7 @@ exports.teamsToolDefinitions = [
|
|
|
90
90
|
path: { type: "string", description: "ADO API path after /{org}, e.g. /_apis/wit/wiql" },
|
|
91
91
|
method: { type: "string", enum: ["GET", "POST"], description: "HTTP method (defaults to GET)" },
|
|
92
92
|
body: { type: "string", description: "JSON request body (optional, used with POST for WIQL)" },
|
|
93
|
+
host: { type: "string", description: "API host override for non-standard APIs (e.g. 'vsapm.dev.azure.com' for entitlements, 'vssps.dev.azure.com' for users). Omit for standard dev.azure.com." },
|
|
93
94
|
},
|
|
94
95
|
required: ["organization", "path"],
|
|
95
96
|
},
|
|
@@ -100,7 +101,7 @@ exports.teamsToolDefinitions = [
|
|
|
100
101
|
return "AUTH_REQUIRED:ado -- I need access to Azure DevOps. Please sign in when prompted.";
|
|
101
102
|
}
|
|
102
103
|
const method = args.method || "GET";
|
|
103
|
-
const result = await (0, ado_client_1.adoRequest)(ctx.adoToken, method, args.organization, args.path, args.body);
|
|
104
|
+
const result = await (0, ado_client_1.adoRequest)(ctx.adoToken, method, args.organization, args.path, args.body, args.host);
|
|
104
105
|
checkAndRecord403(result, "ado", args.organization, method, ctx);
|
|
105
106
|
return result;
|
|
106
107
|
},
|
|
@@ -111,7 +112,7 @@ exports.teamsToolDefinitions = [
|
|
|
111
112
|
type: "function",
|
|
112
113
|
function: {
|
|
113
114
|
name: "ado_mutate",
|
|
114
|
-
description: "POST/PATCH/DELETE any Azure DevOps API endpoint for actual mutations. Use ado_docs first to look up the correct path.",
|
|
115
|
+
description: "POST/PATCH/DELETE any Azure DevOps API endpoint for actual mutations. Use ado_docs first to look up the correct path and host.",
|
|
115
116
|
parameters: {
|
|
116
117
|
type: "object",
|
|
117
118
|
properties: {
|
|
@@ -119,6 +120,7 @@ exports.teamsToolDefinitions = [
|
|
|
119
120
|
organization: { type: "string", description: "Azure DevOps organization name" },
|
|
120
121
|
path: { type: "string", description: "ADO API path after /{org}" },
|
|
121
122
|
body: { type: "string", description: "JSON request body (optional)" },
|
|
123
|
+
host: { type: "string", description: "API host override for non-standard APIs (e.g. 'vsapm.dev.azure.com' for entitlements, 'vssps.dev.azure.com' for users). Omit for standard dev.azure.com." },
|
|
122
124
|
},
|
|
123
125
|
required: ["method", "organization", "path"],
|
|
124
126
|
},
|
|
@@ -133,7 +135,7 @@ exports.teamsToolDefinitions = [
|
|
|
133
135
|
}
|
|
134
136
|
/* v8 ignore next -- fallback unreachable: method is validated against MUTATE_METHODS above @preserve */
|
|
135
137
|
const action = METHOD_TO_ACTION[args.method] || args.method;
|
|
136
|
-
const result = await (0, ado_client_1.adoRequest)(ctx.adoToken, args.method, args.organization, args.path, args.body);
|
|
138
|
+
const result = await (0, ado_client_1.adoRequest)(ctx.adoToken, args.method, args.organization, args.path, args.body, args.host);
|
|
137
139
|
checkAndRecord403(result, "ado", args.organization, action, ctx);
|
|
138
140
|
return result;
|
|
139
141
|
},
|
|
@@ -201,6 +203,53 @@ exports.teamsToolDefinitions = [
|
|
|
201
203
|
},
|
|
202
204
|
integration: "ado",
|
|
203
205
|
},
|
|
206
|
+
// -- Proactive messaging --
|
|
207
|
+
{
|
|
208
|
+
tool: {
|
|
209
|
+
type: "function",
|
|
210
|
+
function: {
|
|
211
|
+
name: "teams_send_message",
|
|
212
|
+
description: "send a proactive 1:1 Teams message to a user. requires their AAD object ID (use graph_query /users to find it). the message appears as coming from the bot.",
|
|
213
|
+
parameters: {
|
|
214
|
+
type: "object",
|
|
215
|
+
properties: {
|
|
216
|
+
user_id: { type: "string", description: "AAD object ID of the user to message" },
|
|
217
|
+
user_name: { type: "string", description: "display name of the user (for logging)" },
|
|
218
|
+
message: { type: "string", description: "message text to send" },
|
|
219
|
+
tenant_id: { type: "string", description: "tenant ID (optional, defaults to current conversation tenant)" },
|
|
220
|
+
},
|
|
221
|
+
required: ["user_id", "message"],
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
/* v8 ignore start -- proactive messaging requires live Teams SDK conversation client @preserve */
|
|
226
|
+
handler: async (args, ctx) => {
|
|
227
|
+
if (!ctx?.botApi) {
|
|
228
|
+
return "proactive messaging is not available -- no bot API context (this tool only works in the Teams channel)";
|
|
229
|
+
}
|
|
230
|
+
try {
|
|
231
|
+
const tenantId = args.tenant_id || ctx.tenantId;
|
|
232
|
+
// Cast to the SDK's ConversationClient shape (kept as `unknown` in ToolContext to avoid type coupling)
|
|
233
|
+
const conversations = ctx.botApi.conversations;
|
|
234
|
+
const conversation = await conversations.create({
|
|
235
|
+
bot: { id: ctx.botApi.id },
|
|
236
|
+
members: [{ id: args.user_id, role: "user", name: args.user_name || args.user_id }],
|
|
237
|
+
tenantId,
|
|
238
|
+
isGroup: false,
|
|
239
|
+
});
|
|
240
|
+
await conversations.activities(conversation.id).create({
|
|
241
|
+
type: "message",
|
|
242
|
+
text: args.message,
|
|
243
|
+
});
|
|
244
|
+
return `message sent to ${args.user_name || args.user_id}`;
|
|
245
|
+
}
|
|
246
|
+
catch (e) {
|
|
247
|
+
return `failed to send proactive message: ${e instanceof Error ? e.message : String(e)}`;
|
|
248
|
+
}
|
|
249
|
+
},
|
|
250
|
+
/* v8 ignore stop */
|
|
251
|
+
confirmationRequired: true,
|
|
252
|
+
},
|
|
204
253
|
// -- Documentation tools --
|
|
205
254
|
{
|
|
206
255
|
tool: {
|
|
@@ -268,6 +317,8 @@ function searchEndpoints(entries, query) {
|
|
|
268
317
|
` ${e.description}`,
|
|
269
318
|
` Params: ${e.params || "none"}`,
|
|
270
319
|
];
|
|
320
|
+
if (e.host)
|
|
321
|
+
lines.push(` Host: ${e.host}`);
|
|
271
322
|
if (e.scopes)
|
|
272
323
|
lines.push(` Scopes: ${e.scopes}`);
|
|
273
324
|
return lines.join("\n");
|
|
@@ -304,5 +355,7 @@ function summarizeTeamsArgs(name, args) {
|
|
|
304
355
|
return summarizeKeyValues(["query"]);
|
|
305
356
|
if (name === "ado_docs")
|
|
306
357
|
return summarizeKeyValues(["query"]);
|
|
358
|
+
if (name === "teams_send_message")
|
|
359
|
+
return summarizeKeyValues(["user_name", "user_id"]);
|
|
307
360
|
return undefined;
|
|
308
361
|
}
|
package/dist/repertoire/tools.js
CHANGED
|
@@ -45,7 +45,9 @@ function getToolsForChannel(capabilities, toolPreferences) {
|
|
|
45
45
|
return baseTools;
|
|
46
46
|
}
|
|
47
47
|
const available = new Set(capabilities.availableIntegrations);
|
|
48
|
-
const
|
|
48
|
+
const channelDefs = [...tools_teams_1.teamsToolDefinitions, ...ado_semantic_1.adoSemanticToolDefinitions, ...tools_github_1.githubToolDefinitions];
|
|
49
|
+
// Include tools whose integration is available, plus channel tools with no integration gate (e.g. teams_send_message)
|
|
50
|
+
const integrationDefs = channelDefs.filter((d) => d.integration ? available.has(d.integration) : capabilities.channel === "teams");
|
|
49
51
|
if (!toolPreferences || Object.keys(toolPreferences).length === 0) {
|
|
50
52
|
return [...baseTools, ...integrationDefs.map((d) => d.tool)];
|
|
51
53
|
}
|
|
@@ -195,5 +197,13 @@ function summarizeArgs(name, args) {
|
|
|
195
197
|
}
|
|
196
198
|
if (name === "ado_backlog_list")
|
|
197
199
|
return summarizeKeyValues(args, ["organization", "project"]);
|
|
200
|
+
if (name === "ado_batch_update")
|
|
201
|
+
return summarizeKeyValues(args, ["organization", "project"]);
|
|
202
|
+
if (name === "ado_create_epic" || name === "ado_create_issue")
|
|
203
|
+
return summarizeKeyValues(args, ["organization", "project", "title"]);
|
|
204
|
+
if (name === "ado_move_items")
|
|
205
|
+
return summarizeKeyValues(args, ["organization", "project", "workItemIds"]);
|
|
206
|
+
if (name === "ado_restructure_backlog")
|
|
207
|
+
return summarizeKeyValues(args, ["organization", "project"]);
|
|
198
208
|
return summarizeUnknownArgs(args);
|
|
199
209
|
}
|
package/dist/senses/teams.js
CHANGED
|
@@ -44,6 +44,8 @@ exports.startTeamsApp = startTeamsApp;
|
|
|
44
44
|
const teams_apps_1 = require("@microsoft/teams.apps");
|
|
45
45
|
const teams_dev_1 = require("@microsoft/teams.dev");
|
|
46
46
|
const core_1 = require("../heart/core");
|
|
47
|
+
const tools_1 = require("../repertoire/tools");
|
|
48
|
+
const channel_1 = require("../mind/friends/channel");
|
|
47
49
|
const config_1 = require("../heart/config");
|
|
48
50
|
const prompt_1 = require("../mind/prompt");
|
|
49
51
|
const phrases_1 = require("../mind/phrases");
|
|
@@ -58,6 +60,7 @@ const resolver_1 = require("../mind/friends/resolver");
|
|
|
58
60
|
const tokens_1 = require("../mind/friends/tokens");
|
|
59
61
|
const turn_coordinator_1 = require("../heart/turn-coordinator");
|
|
60
62
|
const identity_1 = require("../heart/identity");
|
|
63
|
+
const http = __importStar(require("http"));
|
|
61
64
|
const path = __importStar(require("path"));
|
|
62
65
|
const trust_gate_1 = require("./trust-gate");
|
|
63
66
|
// Strip @mention markup from incoming messages.
|
|
@@ -317,7 +320,14 @@ function createTeamsCallbacks(stream, controller, sendMessage, options) {
|
|
|
317
320
|
onToolStart: (name, args) => {
|
|
318
321
|
stopPhraseRotation();
|
|
319
322
|
flushTextBuffer();
|
|
320
|
-
|
|
323
|
+
// Emit a placeholder to satisfy the 15s Copilot timeout for initial
|
|
324
|
+
// stream.emit(). Without this, long tool chains (e.g. ADO batch ops)
|
|
325
|
+
// never emit before the timeout and the user sees "this response was
|
|
326
|
+
// stopped". The placeholder is replaced by actual content on next emit.
|
|
327
|
+
// https://learn.microsoft.com/en-us/answers/questions/2288017/m365-custom-engine-agents-timeout-message-after-15
|
|
328
|
+
if (!streamHasContent)
|
|
329
|
+
safeEmit("⏳");
|
|
330
|
+
const argSummary = (0, tools_1.summarizeArgs)(name, args) || Object.keys(args).join(", ");
|
|
321
331
|
safeUpdate(`running ${name} (${argSummary})...`);
|
|
322
332
|
hadToolRun = true;
|
|
323
333
|
},
|
|
@@ -421,7 +431,12 @@ async function withConversationLock(convId, fn) {
|
|
|
421
431
|
// Create a fresh friend store per request so mkdirSync re-runs if directories
|
|
422
432
|
// are deleted while the process is alive.
|
|
423
433
|
function getFriendStore() {
|
|
424
|
-
|
|
434
|
+
// On Azure App Service, os.homedir() returns /root which is ephemeral.
|
|
435
|
+
// Use /home/.agentstate/ (persistent) when WEBSITE_SITE_NAME is set.
|
|
436
|
+
/* v8 ignore next 3 -- Azure vs local path branch; environment-specific @preserve */
|
|
437
|
+
const friendsPath = process.env.WEBSITE_SITE_NAME
|
|
438
|
+
? path.join("/home", ".agentstate", (0, identity_1.getAgentName)(), "friends")
|
|
439
|
+
: path.join((0, identity_1.getAgentRoot)(), "friends");
|
|
425
440
|
return new store_file_1.FileFriendStore(friendsPath);
|
|
426
441
|
}
|
|
427
442
|
// Handle an incoming Teams message
|
|
@@ -445,6 +460,8 @@ async function handleTeamsMessage(text, stream, conversationId, teamsContext, se
|
|
|
445
460
|
signin: teamsContext.signin,
|
|
446
461
|
friendStore: store,
|
|
447
462
|
summarize: (0, core_1.createSummarize)(),
|
|
463
|
+
tenantId: teamsContext.tenantId,
|
|
464
|
+
botApi: teamsContext.botApi,
|
|
448
465
|
} : undefined;
|
|
449
466
|
if (toolContext) {
|
|
450
467
|
const resolver = new resolver_1.FriendResolver(store, {
|
|
@@ -497,6 +514,8 @@ async function handleTeamsMessage(text, stream, conversationId, teamsContext, se
|
|
|
497
514
|
const messages = existing?.messages && existing.messages.length > 0
|
|
498
515
|
? existing.messages
|
|
499
516
|
: [{ role: "system", content: await (0, prompt_1.buildSystem)("teams", undefined, toolContext?.context) }];
|
|
517
|
+
// Repair any orphaned tool calls from a previous aborted turn
|
|
518
|
+
(0, core_1.repairOrphanedToolCalls)(messages);
|
|
500
519
|
// Push user message
|
|
501
520
|
messages.push({ role: "user", content: text });
|
|
502
521
|
// Run agent
|
|
@@ -518,12 +537,12 @@ async function handleTeamsMessage(text, stream, conversationId, teamsContext, se
|
|
|
518
537
|
// This must happen after the stream is done so the OAuth card renders properly.
|
|
519
538
|
if (teamsContext) {
|
|
520
539
|
const allContent = messages.map(m => typeof m.content === "string" ? m.content : "").join("\n");
|
|
521
|
-
if (allContent.includes("AUTH_REQUIRED:graph"))
|
|
522
|
-
await teamsContext.signin(
|
|
523
|
-
if (allContent.includes("AUTH_REQUIRED:ado"))
|
|
524
|
-
await teamsContext.signin(
|
|
525
|
-
if (allContent.includes("AUTH_REQUIRED:github"))
|
|
526
|
-
await teamsContext.signin(
|
|
540
|
+
if (allContent.includes("AUTH_REQUIRED:graph") && teamsContext.graphConnectionName)
|
|
541
|
+
await teamsContext.signin(teamsContext.graphConnectionName);
|
|
542
|
+
if (allContent.includes("AUTH_REQUIRED:ado") && teamsContext.adoConnectionName)
|
|
543
|
+
await teamsContext.signin(teamsContext.adoConnectionName);
|
|
544
|
+
if (allContent.includes("AUTH_REQUIRED:github") && teamsContext.githubConnectionName)
|
|
545
|
+
await teamsContext.signin(teamsContext.githubConnectionName);
|
|
527
546
|
}
|
|
528
547
|
// Trim context and save session
|
|
529
548
|
(0, context_1.postTurn)(messages, sessPath, result.usage);
|
|
@@ -533,49 +552,85 @@ async function handleTeamsMessage(text, stream, conversationId, teamsContext, se
|
|
|
533
552
|
}
|
|
534
553
|
// SDK auto-closes the stream after our handler returns (app.process.js)
|
|
535
554
|
}
|
|
536
|
-
//
|
|
537
|
-
//
|
|
538
|
-
|
|
539
|
-
|
|
555
|
+
// Internal port for the secondary bot App (not exposed externally).
|
|
556
|
+
// The primary app proxies /api/messages-secondary → localhost:SECONDARY_PORT/api/messages.
|
|
557
|
+
const SECONDARY_INTERNAL_PORT = 3979;
|
|
558
|
+
// Collect all unique OAuth connection names across top-level config and tenant overrides.
|
|
559
|
+
/* v8 ignore start -- runtime Teams SDK config; no unit-testable surface @preserve */
|
|
560
|
+
function allOAuthConnectionNames() {
|
|
561
|
+
const oauthConfig = (0, config_1.getOAuthConfig)();
|
|
562
|
+
const names = new Set();
|
|
563
|
+
if (oauthConfig.graphConnectionName)
|
|
564
|
+
names.add(oauthConfig.graphConnectionName);
|
|
565
|
+
if (oauthConfig.adoConnectionName)
|
|
566
|
+
names.add(oauthConfig.adoConnectionName);
|
|
567
|
+
if (oauthConfig.githubConnectionName)
|
|
568
|
+
names.add(oauthConfig.githubConnectionName);
|
|
569
|
+
if (oauthConfig.tenantOverrides) {
|
|
570
|
+
for (const ov of Object.values(oauthConfig.tenantOverrides)) {
|
|
571
|
+
if (ov.graphConnectionName)
|
|
572
|
+
names.add(ov.graphConnectionName);
|
|
573
|
+
if (ov.adoConnectionName)
|
|
574
|
+
names.add(ov.adoConnectionName);
|
|
575
|
+
if (ov.githubConnectionName)
|
|
576
|
+
names.add(ov.githubConnectionName);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
return [...names];
|
|
580
|
+
}
|
|
581
|
+
// Create an App instance from a TeamsConfig. Returns { app, mode }.
|
|
582
|
+
function createBotApp(teamsConfig) {
|
|
540
583
|
const mentionStripping = { activity: { mentions: { stripText: true } } };
|
|
541
|
-
const teamsConfig = (0, config_2.getTeamsConfig)();
|
|
542
|
-
let app;
|
|
543
|
-
let mode;
|
|
544
584
|
const oauthConfig = (0, config_1.getOAuthConfig)();
|
|
545
|
-
if (teamsConfig.clientId) {
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
585
|
+
if (teamsConfig.clientId && teamsConfig.clientSecret) {
|
|
586
|
+
return {
|
|
587
|
+
app: new teams_apps_1.App({
|
|
588
|
+
clientId: teamsConfig.clientId,
|
|
589
|
+
clientSecret: teamsConfig.clientSecret,
|
|
590
|
+
tenantId: teamsConfig.tenantId,
|
|
591
|
+
oauth: { defaultConnectionName: oauthConfig.graphConnectionName },
|
|
592
|
+
...mentionStripping,
|
|
593
|
+
}),
|
|
594
|
+
mode: "Bot Service (client secret)",
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
else if (teamsConfig.clientId) {
|
|
598
|
+
return {
|
|
599
|
+
app: new teams_apps_1.App({
|
|
600
|
+
clientId: teamsConfig.clientId,
|
|
601
|
+
tenantId: teamsConfig.tenantId,
|
|
602
|
+
...(teamsConfig.managedIdentityClientId ? { managedIdentityClientId: teamsConfig.managedIdentityClientId } : {}),
|
|
603
|
+
oauth: { defaultConnectionName: oauthConfig.graphConnectionName },
|
|
604
|
+
...mentionStripping,
|
|
605
|
+
}),
|
|
606
|
+
mode: "Bot Service (managed identity)",
|
|
607
|
+
};
|
|
555
608
|
}
|
|
556
609
|
else {
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
610
|
+
return {
|
|
611
|
+
app: new teams_apps_1.App({
|
|
612
|
+
plugins: [new teams_dev_1.DevtoolsPlugin()],
|
|
613
|
+
...mentionStripping,
|
|
614
|
+
}),
|
|
615
|
+
mode: "DevtoolsPlugin",
|
|
616
|
+
};
|
|
563
617
|
}
|
|
618
|
+
}
|
|
619
|
+
/* v8 ignore stop */
|
|
620
|
+
// Register message, verify-state, and error handlers on an App instance.
|
|
621
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
622
|
+
function registerBotHandlers(app, label) {
|
|
623
|
+
const connectionNames = allOAuthConnectionNames();
|
|
564
624
|
// Override default OAuth verify-state handler. The SDK's built-in handler
|
|
565
625
|
// uses a single defaultConnectionName, which breaks multi-connection setups
|
|
566
626
|
// (graph + ado + github). The verifyState activity only carries a `state`
|
|
567
627
|
// code with no connectionName, so we try each configured connection until
|
|
568
628
|
// one succeeds.
|
|
569
|
-
const allConnectionNames = [
|
|
570
|
-
oauthConfig.graphConnectionName,
|
|
571
|
-
oauthConfig.adoConnectionName,
|
|
572
|
-
oauthConfig.githubConnectionName,
|
|
573
|
-
].filter(Boolean);
|
|
574
629
|
app.on("signin.verify-state", async (ctx) => {
|
|
575
630
|
const { api, activity } = ctx;
|
|
576
631
|
if (!activity.value?.state)
|
|
577
632
|
return { status: 404 };
|
|
578
|
-
for (const cn of
|
|
633
|
+
for (const cn of connectionNames) {
|
|
579
634
|
try {
|
|
580
635
|
await api.users.token.get({
|
|
581
636
|
channelId: activity.channelId,
|
|
@@ -583,12 +638,12 @@ function startTeamsApp() {
|
|
|
583
638
|
connectionName: cn,
|
|
584
639
|
code: activity.value.state,
|
|
585
640
|
});
|
|
586
|
-
(0, runtime_1.emitNervesEvent)({ level: "info", event: "channel.verify_state", component: "channels", message: `verify-state succeeded for connection "${cn}"`, meta: { connectionName: cn } });
|
|
641
|
+
(0, runtime_1.emitNervesEvent)({ level: "info", event: "channel.verify_state", component: "channels", message: `[${label}] verify-state succeeded for connection "${cn}"`, meta: { connectionName: cn } });
|
|
587
642
|
return { status: 200 };
|
|
588
643
|
}
|
|
589
644
|
catch { /* try next */ }
|
|
590
645
|
}
|
|
591
|
-
(0, runtime_1.emitNervesEvent)({ level: "warn", event: "channel.verify_state", component: "channels", message:
|
|
646
|
+
(0, runtime_1.emitNervesEvent)({ level: "warn", event: "channel.verify_state", component: "channels", message: `[${label}] verify-state failed for all connections`, meta: {} });
|
|
592
647
|
return { status: 412 };
|
|
593
648
|
});
|
|
594
649
|
app.on("message", async (ctx) => {
|
|
@@ -598,16 +653,12 @@ function startTeamsApp() {
|
|
|
598
653
|
const turnKey = teamsTurnKey(convId);
|
|
599
654
|
const userId = activity.from?.id || "";
|
|
600
655
|
const channelId = activity.channelId || "msteams";
|
|
601
|
-
(0, runtime_1.emitNervesEvent)({ level: "info", event: "channel.message_received", component: "channels", message:
|
|
656
|
+
(0, runtime_1.emitNervesEvent)({ level: "info", event: "channel.message_received", component: "channels", message: `[${label}] incoming teams message`, meta: { userId: userId.slice(0, 12), conversationId: convId.slice(0, 20) } });
|
|
602
657
|
// Resolve pending confirmations IMMEDIATELY — before token fetches or
|
|
603
658
|
// the conversation lock. The original message holds the lock while
|
|
604
659
|
// awaiting confirmation, so acquiring it here would deadlock. Token
|
|
605
660
|
// fetches are also unnecessary (and slow) for a simple yes/no reply.
|
|
606
661
|
if (resolvePendingConfirmation(convId, text)) {
|
|
607
|
-
// Don't emit on this stream — the original message's stream is still
|
|
608
|
-
// active. Opening a second streaming response in the same conversation
|
|
609
|
-
// can corrupt the first. The original stream will show tool progress
|
|
610
|
-
// once the confirmation Promise resolves.
|
|
611
662
|
return;
|
|
612
663
|
}
|
|
613
664
|
// If this conversation already has an active turn, steer follow-up input
|
|
@@ -621,27 +672,30 @@ function startTeamsApp() {
|
|
|
621
672
|
return;
|
|
622
673
|
}
|
|
623
674
|
try {
|
|
675
|
+
// Resolve OAuth connection names for this user's tenant (supports per-tenant overrides).
|
|
676
|
+
const tenantId = activity.conversation?.tenantId;
|
|
677
|
+
const tenantOAuth = (0, config_1.resolveOAuthForTenant)(tenantId);
|
|
624
678
|
// Fetch tokens for both OAuth connections independently.
|
|
625
679
|
// Failures are silently caught -- the tool handler will request signin if needed.
|
|
626
680
|
let graphToken;
|
|
627
681
|
let adoToken;
|
|
628
682
|
let githubToken;
|
|
629
683
|
try {
|
|
630
|
-
const graphRes = await api.users.token.get({ userId, connectionName:
|
|
684
|
+
const graphRes = await api.users.token.get({ userId, connectionName: tenantOAuth.graphConnectionName, channelId });
|
|
631
685
|
graphToken = graphRes?.token;
|
|
632
686
|
}
|
|
633
687
|
catch { /* no token yet — tool handler will trigger signin */ }
|
|
634
688
|
try {
|
|
635
|
-
const adoRes = await api.users.token.get({ userId, connectionName:
|
|
689
|
+
const adoRes = await api.users.token.get({ userId, connectionName: tenantOAuth.adoConnectionName, channelId });
|
|
636
690
|
adoToken = adoRes?.token;
|
|
637
691
|
}
|
|
638
692
|
catch { /* no token yet — tool handler will trigger signin */ }
|
|
639
693
|
try {
|
|
640
|
-
const githubRes = await api.users.token.get({ userId, connectionName:
|
|
694
|
+
const githubRes = await api.users.token.get({ userId, connectionName: tenantOAuth.githubConnectionName, channelId });
|
|
641
695
|
githubToken = githubRes?.token;
|
|
642
696
|
}
|
|
643
697
|
catch { /* no token yet — tool handler will trigger signin */ }
|
|
644
|
-
(0, runtime_1.emitNervesEvent)({ level: "info", event: "channel.token_status", component: "channels", message: "oauth token availability", meta: { graph: !!graphToken, ado: !!adoToken, github: !!githubToken } });
|
|
698
|
+
(0, runtime_1.emitNervesEvent)({ level: "info", event: "channel.token_status", component: "channels", message: "oauth token availability", meta: { graph: !!graphToken, ado: !!adoToken, github: !!githubToken, tenantId } });
|
|
645
699
|
const teamsContext = {
|
|
646
700
|
graphToken,
|
|
647
701
|
adoToken,
|
|
@@ -661,6 +715,11 @@ function startTeamsApp() {
|
|
|
661
715
|
aadObjectId: activity.from?.aadObjectId,
|
|
662
716
|
tenantId: activity.conversation?.tenantId,
|
|
663
717
|
displayName: activity.from?.name,
|
|
718
|
+
graphConnectionName: tenantOAuth.graphConnectionName,
|
|
719
|
+
adoConnectionName: tenantOAuth.adoConnectionName,
|
|
720
|
+
githubConnectionName: tenantOAuth.githubConnectionName,
|
|
721
|
+
/* v8 ignore next -- bot API availability branch; requires live SDK context @preserve */
|
|
722
|
+
botApi: app.id && api ? { id: app.id, conversations: api.conversations } : undefined,
|
|
664
723
|
};
|
|
665
724
|
/* v8 ignore next 5 -- bot-framework integration callback; tested via handleTeamsMessage sendMessage path @preserve */
|
|
666
725
|
const ctxSend = async (t) => {
|
|
@@ -678,6 +737,23 @@ function startTeamsApp() {
|
|
|
678
737
|
_turnCoordinator.endTurn(turnKey);
|
|
679
738
|
}
|
|
680
739
|
});
|
|
740
|
+
app.event("error", ({ error }) => {
|
|
741
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
742
|
+
(0, runtime_1.emitNervesEvent)({ level: "error", event: "channel.app_error", component: "channels", message: `[${label}] ${msg}`, meta: {} });
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
// Start the Teams app in DevtoolsPlugin mode (local dev) or Bot Service mode (real Teams).
|
|
746
|
+
// Mode is determined by getTeamsConfig().clientId.
|
|
747
|
+
// Text is always accumulated in textBuffer and flushed periodically (chunked streaming).
|
|
748
|
+
//
|
|
749
|
+
// Dual-bot support: if teamsSecondary is configured with a clientId, a second App
|
|
750
|
+
// instance starts on an internal port and the primary app proxies requests from
|
|
751
|
+
// /api/messages-secondary to it. This lets a single App Service serve two bot
|
|
752
|
+
// registrations (e.g. one per tenant) without SDK modifications.
|
|
753
|
+
function startTeamsApp() {
|
|
754
|
+
const teamsConfig = (0, config_2.getTeamsConfig)();
|
|
755
|
+
const { app, mode } = createBotApp(teamsConfig);
|
|
756
|
+
registerBotHandlers(app, "primary");
|
|
681
757
|
if (!process.listeners("unhandledRejection").some((l) => l.__agentHandler)) {
|
|
682
758
|
const handler = (err) => {
|
|
683
759
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -686,11 +762,54 @@ function startTeamsApp() {
|
|
|
686
762
|
handler.__agentHandler = true;
|
|
687
763
|
process.on("unhandledRejection", handler);
|
|
688
764
|
}
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
(0, runtime_1.emitNervesEvent)({ level: "error", event: "channel.app_error", component: "channels", message: msg, meta: {} });
|
|
692
|
-
});
|
|
693
|
-
const port = (0, config_2.getTeamsChannelConfig)().port;
|
|
765
|
+
/* v8 ignore next -- PORT env branch; runtime-only @preserve */
|
|
766
|
+
const port = process.env.PORT ? Number(process.env.PORT) : (0, config_2.getTeamsChannelConfig)().port;
|
|
694
767
|
app.start(port);
|
|
695
|
-
|
|
768
|
+
// Diagnostic: log tool count at startup to verify deploy
|
|
769
|
+
const startupTools = (0, tools_1.getToolsForChannel)((0, channel_1.getChannelCapabilities)("teams"));
|
|
770
|
+
const toolNames = startupTools.map((t) => t.function.name);
|
|
771
|
+
(0, runtime_1.emitNervesEvent)({ level: "info", event: "channel.app_started", component: "channels", message: `Teams bot started on port ${port} with ${mode} (chunked streaming)`, meta: { port, mode, toolCount: toolNames.length, hasProactive: toolNames.includes("teams_send_message") } });
|
|
772
|
+
// --- Secondary bot (dual-bot support) ---
|
|
773
|
+
// If teamsSecondary has a clientId, start a second App on an internal port
|
|
774
|
+
// and proxy /api/messages-secondary on the primary app to it.
|
|
775
|
+
/* v8 ignore start -- dual-bot proxy wiring; requires live Teams SDK + HTTP @preserve */
|
|
776
|
+
const secondaryConfig = (0, config_1.getTeamsSecondaryConfig)();
|
|
777
|
+
if (secondaryConfig.clientId) {
|
|
778
|
+
const { app: secondaryApp, mode: secondaryMode } = createBotApp(secondaryConfig);
|
|
779
|
+
registerBotHandlers(secondaryApp, "secondary");
|
|
780
|
+
secondaryApp.start(SECONDARY_INTERNAL_PORT);
|
|
781
|
+
(0, runtime_1.emitNervesEvent)({ level: "info", event: "channel.app_started", component: "channels", message: `Secondary bot started on internal port ${SECONDARY_INTERNAL_PORT} with ${secondaryMode}`, meta: { port: SECONDARY_INTERNAL_PORT, mode: secondaryMode } });
|
|
782
|
+
// Proxy: forward /api/messages-secondary on the primary app's Express
|
|
783
|
+
// to localhost:SECONDARY_INTERNAL_PORT/api/messages.
|
|
784
|
+
// The SDK's HttpPlugin exposes .post() bound to its Express instance.
|
|
785
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
786
|
+
const httpPlugin = app.http;
|
|
787
|
+
httpPlugin.post("/api/messages-secondary", (req, res) => {
|
|
788
|
+
const body = JSON.stringify(req.body);
|
|
789
|
+
const proxyReq = http.request({
|
|
790
|
+
hostname: "127.0.0.1",
|
|
791
|
+
port: SECONDARY_INTERNAL_PORT,
|
|
792
|
+
path: "/api/messages",
|
|
793
|
+
method: "POST",
|
|
794
|
+
headers: {
|
|
795
|
+
...req.headers,
|
|
796
|
+
"content-length": Buffer.byteLength(body).toString(),
|
|
797
|
+
},
|
|
798
|
+
}, (proxyRes) => {
|
|
799
|
+
res.writeHead(proxyRes.statusCode ?? 200, proxyRes.headers);
|
|
800
|
+
proxyRes.pipe(res);
|
|
801
|
+
});
|
|
802
|
+
proxyReq.on("error", (err) => {
|
|
803
|
+
(0, runtime_1.emitNervesEvent)({ level: "error", event: "channel.proxy_error", component: "channels", message: `secondary proxy error: ${err.message}`, meta: {} });
|
|
804
|
+
if (!res.headersSent) {
|
|
805
|
+
res.writeHead(502);
|
|
806
|
+
res.end("Bad Gateway");
|
|
807
|
+
}
|
|
808
|
+
});
|
|
809
|
+
proxyReq.write(body);
|
|
810
|
+
proxyReq.end();
|
|
811
|
+
});
|
|
812
|
+
(0, runtime_1.emitNervesEvent)({ level: "info", event: "channel.proxy_ready", component: "channels", message: "proxy /api/messages-secondary → secondary bot ready", meta: {} });
|
|
813
|
+
}
|
|
814
|
+
/* v8 ignore stop */
|
|
696
815
|
}
|