From 075b216006ed67fe40cd8e626c13d92a83e81546 Mon Sep 17 00:00:00 2001 From: Walter Luna Date: Tue, 11 Nov 2025 11:21:31 +0000 Subject: [PATCH 01/13] Add devin agent sample --- nodejs/devin/.env.example | 7 + nodejs/devin/README.md | 1 + nodejs/devin/package.json | 30 +++ nodejs/devin/src/agent.ts | 199 +++++++++++++++++++ nodejs/devin/src/devin-client.ts | 139 +++++++++++++ nodejs/devin/src/index.ts | 46 +++++ nodejs/devin/src/types/devin-client.types.ts | 14 ++ nodejs/devin/src/utils.ts | 55 +++++ nodejs/devin/tsconfig.json | 20 ++ 9 files changed, 511 insertions(+) create mode 100644 nodejs/devin/.env.example create mode 100644 nodejs/devin/README.md create mode 100644 nodejs/devin/package.json create mode 100644 nodejs/devin/src/agent.ts create mode 100644 nodejs/devin/src/devin-client.ts create mode 100644 nodejs/devin/src/index.ts create mode 100644 nodejs/devin/src/types/devin-client.types.ts create mode 100644 nodejs/devin/src/utils.ts create mode 100644 nodejs/devin/tsconfig.json diff --git a/nodejs/devin/.env.example b/nodejs/devin/.env.example new file mode 100644 index 00000000..4f7a1993 --- /dev/null +++ b/nodejs/devin/.env.example @@ -0,0 +1,7 @@ +# Devin API Configuration +DEVIN_BASE_URL=https://api.devin.ai/v1 +DEVIN_API_KEY=your_devin_api_key_here + +# Polling interval in seconds (how often to check for Devin responses) +POLLING_INTERVAL_SECONDS=10 + diff --git a/nodejs/devin/README.md b/nodejs/devin/README.md new file mode 100644 index 00000000..1ea1912b --- /dev/null +++ b/nodejs/devin/README.md @@ -0,0 +1 @@ +TODO diff --git a/nodejs/devin/package.json b/nodejs/devin/package.json new file mode 100644 index 00000000..64176bfa --- /dev/null +++ b/nodejs/devin/package.json @@ -0,0 +1,30 @@ +{ + "name": "devin-agent-sample", + "version": "1.0.0", + "main": "index.js", + "scripts": { + "build": "tsc", + "start": "node --env-file=.env dist/index.js", + "test-tool": "agentsplayground", + "install:clean": "npm run clean && npm install", + "clean": "rimraf dist node_modules package-lock.json" + }, + "keywords": [], + "author": "walterluna@microsoft.com", + "license": "ISC", + "description": "", + "dependencies": { + "@microsoft/agents-a365-notifications": "*", + "@microsoft/agents-a365-observability": "*", + "@microsoft/agents-a365-runtime": "*", + "@microsoft/agents-a365-tooling": "*", + "@microsoft/agents-hosting": "^1.0.15" + }, + "devDependencies": { + "@microsoft/m365agentsplayground": "^0.2.18", + "nodemon": "^3.1.10", + "rimraf": "^5.0.0", + "ts-node": "^10.9.2", + "typescript": "^5.9.2" + } +} \ No newline at end of file diff --git a/nodejs/devin/src/agent.ts b/nodejs/devin/src/agent.ts new file mode 100644 index 00000000..4ef30887 --- /dev/null +++ b/nodejs/devin/src/agent.ts @@ -0,0 +1,199 @@ +import { + AgentDetails, + BaggageBuilder, + InferenceDetails, + InferenceOperationType, + InferenceScope, + InvokeAgentScope, + TenantDetails, +} from "@microsoft/agents-a365-observability"; +import { Activity, ActivityTypes } from "@microsoft/agents-activity"; +import { + AgentApplication, + DefaultConversationState, + TurnContext, + TurnState, +} from "@microsoft/agents-hosting"; +import { Stream } from "stream"; +import { devinClient } from "./devin-client"; +import { getAgentDetails, getTenantDetails } from "./utils"; + +interface ConversationState extends DefaultConversationState { + count: number; +} +type ApplicationTurnState = TurnState; + +export class A365Agent extends AgentApplication { + isApplicationInstalled: boolean = false; + termsAndConditionsAccepted: boolean = false; + agentName = "Devin Agent"; + + constructor() { + super(); + + this.onActivity( + ActivityTypes.Message, + async (context: TurnContext, state: ApplicationTurnState) => { + // Increment count state + let count = state.conversation.count ?? 0; + state.conversation.count = ++count; + // Extract agent and tenant details from context + const invokeAgentDetails = getAgentDetails(context); + const tenantDetails = getTenantDetails(context); + + // Create BaggageBuilder scope + const baggageScope = new BaggageBuilder() + .tenantId(tenantDetails.tenantId) + .agentId(invokeAgentDetails.agentId) + .correlationId("73f5b5df-866e-4568-8ac7-ae0d36fc1de2") // TODO: ask, should a new uuid be generated each call? + .agentName(invokeAgentDetails.agentName) + .conversationId(context.activity.conversation?.id) + .build(); + + await baggageScope.run(async () => { + const invokeAgentScope = InvokeAgentScope.start( + invokeAgentDetails, + tenantDetails + ); + + await invokeAgentScope.withActiveSpanAsync(async () => { + invokeAgentScope.recordInputMessages([ + context.activity.text ?? "Unknown text", + ]); + + await context.sendActivity(Activity.fromObject({ type: "typing" })); // TODO: figure out typing + + await this.handleAgentMessageActivity( + context, + invokeAgentScope, + invokeAgentDetails, + tenantDetails + ); + }); + + invokeAgentScope.dispose(); + }); + + baggageScope.dispose(); + } + ); + + this.onActivity( + ActivityTypes.InstallationUpdate, + async (context: TurnContext, state: TurnState) => { + await this.handleInstallationUpdateActivity(context, state); + } + ); + } + + /** + * Handles incoming user messages and sends responses. + */ + async handleAgentMessageActivity( + turnContext: TurnContext, + invokeAgentScope: InvokeAgentScope, + agentDetails: AgentDetails, + tenantDetails: TenantDetails + ): Promise { + if (!this.isApplicationInstalled) { + await turnContext.sendActivity( + "Please install the application before sending messages." + ); + return; + } + + // TODO: Is this terms & conditions really required? + if (!this.termsAndConditionsAccepted) { + if (turnContext.activity.text?.trim().toLowerCase() === "i accept") { + this.termsAndConditionsAccepted = true; + await turnContext.sendActivity( + "Thank you for accepting the terms and conditions! How can I assist you today?" + ); + return; + } else { + await turnContext.sendActivity( + "Please accept the terms and conditions to proceed. Send 'I accept' to accept." + ); + return; + } + } + + const userMessage = turnContext.activity.text?.trim() || ""; + + if (!userMessage) { + await turnContext.sendActivity( + "Please send me a message and I'll help you!" + ); + return; + } + + try { + const inferenceDetails: InferenceDetails = { + operationName: InferenceOperationType.CHAT, + model: "claude-3-7-sonnet-20250219", + providerName: "cognition-ai", + inputTokens: Math.ceil(userMessage.length / 4), // Rough estimate, + responseId: `resp-${Date.now()}`, + outputTokens: 0, // Will be updated after response, + finishReasons: undefined, + }; + + const inferenceScope = InferenceScope.start( + inferenceDetails, + agentDetails, + tenantDetails + ); + + let totalResponseLength = 0; + const responseStream = new Stream() + .on("data", async (chunk) => { + totalResponseLength += (chunk as string).length; + invokeAgentScope.recordOutputMessages([`LLM Response: ${chunk}`]); + inferenceScope.recordOutputMessages([`LLM Response: ${chunk}`]); + await turnContext.sendActivity(chunk); + }) + .on("error", async (error) => { + invokeAgentScope.recordOutputMessages([`Streaming error: ${error}`]); + inferenceScope.recordOutputMessages([`Streaming error: ${error}`]); + await turnContext.sendActivity(error); + }) + .on("close", () => { + inferenceScope.recordOutputTokens(Math.ceil(totalResponseLength / 4)); // Rough estimate + inferenceScope.recordFinishReasons(["stop"]); + }); + + inferenceScope.recordInputMessages([userMessage]); + + await devinClient.invokeAgent(userMessage, responseStream); + } catch (error) { + invokeAgentScope.recordOutputMessages([`LLM error: ${error}`]); + await turnContext.sendActivity( + "There was an error processing your request" + ); + } + } + + /** + * Handles agent installation and removal events. + */ + async handleInstallationUpdateActivity( + turnContext: TurnContext, + state: TurnState + ): Promise { + if (turnContext.activity.action === "add") { + this.isApplicationInstalled = true; + this.termsAndConditionsAccepted = false; + await turnContext.sendActivity( + 'Thank you for hiring me! Looking forward to assisting you in your professional journey! Before I begin, could you please confirm that you accept the terms and conditions? Send "I accept" to accept.' + ); + } else if (turnContext.activity.action === "remove") { + this.isApplicationInstalled = false; + this.termsAndConditionsAccepted = false; + await turnContext.sendActivity( + "Thank you for your time, I enjoyed working with you." + ); + } + } +} + +export const agentApplication = new A365Agent(); diff --git a/nodejs/devin/src/devin-client.ts b/nodejs/devin/src/devin-client.ts new file mode 100644 index 00000000..f6eb334e --- /dev/null +++ b/nodejs/devin/src/devin-client.ts @@ -0,0 +1,139 @@ +import Stream from "stream"; +import { + DevinCreateSessionResponse, + DevinSessionResponse, + DevinSessionStatus, +} from "./types/devin-client.types"; + +export interface Client { + invokeAgent(prompt: string, responseStream: Stream): Promise; +} + +/** + * DevinClient provides an interface to interact with the Devin API + * It maintains agentOptions as an instance field and exposes an invokeAgent method. + */ +export class DevinClient implements Client { + private readonly devinMessageType = "devin_message"; + private readonly devinBaseUrl: string; + private readonly devinApiKey: string; + private readonly pollingIntervalSeconds: number; + private currentSession: string | undefined; + + constructor() { + this.devinBaseUrl = process.env.DEVIN_BASE_URL || ""; + this.devinApiKey = process.env.DEVIN_API_KEY || ""; + this.pollingIntervalSeconds = parseInt( + process.env.POLLING_INTERVAL_SECONDS || "10" + ); + + if (!this.devinBaseUrl) { + throw new Error("DEVIN_BASE_URL environment variable is required"); + } + + if (!this.devinApiKey) { + throw new Error("DEVIN_API_KEY environment variable is required"); + } + } + + /** + * Sends a user message to Devin API and returns the AI's response in a stream. + * Handles streaming results and error reporting. + * + * @param {string} prompt - The message or prompt to send to Devin. + * @param {Stream} responseStream - A stream for the client to send Devin's replies to. + * @returns {Promise} + */ + async invokeAgent(prompt: string, responseStream: Stream): Promise { + const pollMs = this.pollingIntervalSeconds * 1_000 || 10_000; + + this.currentSession = await this.promptDevin(prompt, this.currentSession); + await this.getDevinResponse(this.currentSession, pollMs, responseStream); + } + + private async promptDevin( + prompt: string, + sessionId?: string + ): Promise { + const requestUrl = sessionId + ? `${this.devinBaseUrl}/sessions/${sessionId}/message` + : `${this.devinBaseUrl}/sessions`; + + const requestBody = sessionId ? { message: prompt } : { prompt }; + + const response = await fetch(requestUrl, { + method: "POST", + headers: this.getReqHeaders(), + body: JSON.stringify(requestBody), + }); + + const data = (await response.json()) as DevinCreateSessionResponse; + const rawSessionId = String(data?.session_id ?? ""); + return sessionId || rawSessionId.replace("devin-", ""); + } + + private async getDevinResponse( + sessionId: string, + pollMs: number, + responseStream: Stream, + timeoutMs: number = 300_000 + ): Promise { + const deadline = Date.now() + timeoutMs; + const sentMessages = new Set(); + let latestStatus = DevinSessionStatus.new; + + console.debug("starting poll for Devin's reply"); + + while (Object.values(DevinSessionStatus).includes(latestStatus)) { + console.debug("calling GET session/messages"); + if (Date.now() > deadline) { + console.info("Timed out, not polling for an answer anymore"); + break; + } + + await this.delay(pollMs); + const requestUrl = `${this.devinBaseUrl}/sessions/${sessionId}`; + + const response = await fetch(requestUrl, { + headers: this.getReqHeaders(), + }); + + if (response.status !== 200) { + console.error(`API call failed with status ${response.status}}`); + console.error(`Error response: ${JSON.stringify(response)}}`); + break; // should we break or continue polling? + } + + const data = (await response.json()) as DevinSessionResponse; + latestStatus = data.status; + console.debug(`Current Devin Session status is: ${latestStatus}`); + const latestMessage = data?.messages?.pop(); + console.debug(`latest message is ${JSON.stringify(latestMessage)}`); + + if (latestMessage && latestMessage.type === this.devinMessageType) { + if (!sentMessages.has(latestMessage.event_id)) { + const messageContent = String(latestMessage?.message); + responseStream.emit("data", messageContent); + sentMessages.add(latestMessage.event_id); + console.debug(`emit data event with content: ${messageContent}}`); + } + } + } + + console.debug("emiting close event"); + responseStream.emit("close"); + } + + private delay(ms: number): Promise { + return new Promise((r) => setTimeout(r, ms)); + } + + private getReqHeaders(): Record { + return { + Authorization: `Bearer ${this.devinApiKey}`, + "Content-Type": "application/json", + }; + } +} + +export const devinClient = new DevinClient(); diff --git a/nodejs/devin/src/index.ts b/nodejs/devin/src/index.ts new file mode 100644 index 00000000..3b2ba015 --- /dev/null +++ b/nodejs/devin/src/index.ts @@ -0,0 +1,46 @@ +import { + AuthConfiguration, + authorizeJWT, + CloudAdapter, + loadAuthConfigFromEnv, + Request, +} from "@microsoft/agents-hosting"; +import express, { Response } from "express"; +import { agentApplication } from "./agent"; + +const authConfig: AuthConfiguration = loadAuthConfigFromEnv(); +const adapter = new CloudAdapter(authConfig); + +const app = express(); +app.use(express.json()); +app.use(authorizeJWT(authConfig)); + +app.post("/api/messages", async (req: Request, res: Response) => { + await adapter.process(req, res, async (context) => { + const app = agentApplication; + await app.run(context); + }); +}); + +const port = process.env.PORT || 3978; +const server = app + .listen(port, () => { + console.log( + `\nServer listening to port ${port} for appId ${authConfig.clientId} debug ${process.env.DEBUG}` + ); + }) + .on("error", async (err) => { + console.error(err); + process.exit(1); + }) + .on("close", async () => { + console.log("Server is shutting down..."); + }); + +process.on("SIGINT", () => { + console.log("Received SIGINT. Shutting down gracefully..."); + server.close(() => { + console.log("Server closed."); + process.exit(0); + }); +}); diff --git a/nodejs/devin/src/types/devin-client.types.ts b/nodejs/devin/src/types/devin-client.types.ts new file mode 100644 index 00000000..7a615b06 --- /dev/null +++ b/nodejs/devin/src/types/devin-client.types.ts @@ -0,0 +1,14 @@ +export interface DevinCreateSessionResponse { + session_id?: string; +} + +export interface DevinSessionResponse { + status: DevinSessionStatus; + messages?: { type: string; message?: string; event_id: string }[]; +} + +export enum DevinSessionStatus { + new = "new", + claimed = "claimed", + running = "running", +} diff --git a/nodejs/devin/src/utils.ts b/nodejs/devin/src/utils.ts new file mode 100644 index 00000000..ab756b3e --- /dev/null +++ b/nodejs/devin/src/utils.ts @@ -0,0 +1,55 @@ +import { + ExecutionType, + InvokeAgentDetails, + TenantDetails, +} from "@microsoft/agents-a365-observability"; +import { TurnContext } from "@microsoft/agents-hosting"; + +// Helper functions to extract agent and tenant details from context +export function getAgentDetails(context: TurnContext): InvokeAgentDetails { + // Extract agent ID from activity recipient - use agenticAppId (camelCase, not underscore) + const agentId = + (context.activity.recipient as any)?.agenticAppId || + process.env.AGENT_ID || + "devin-agent"; + + console.log( + `🎯 Agent ID: ${agentId} (from ${ + (context.activity.recipient as any)?.agenticAppId + ? "activity.recipient.agenticAppId" + : "environment/fallback" + })` + ); + + return { + agentId: agentId, + agentName: + (context.activity.recipient as any)?.name || + process.env.AGENT_NAME || + "Devin Agent Sample", + conversationId: context.activity.conversation?.id, + request: { + content: context.activity.text || "Unknown text", + executionType: ExecutionType.HumanToAgent, + sessionId: context.activity.conversation?.id, + }, + }; +} + +export function getTenantDetails(context: TurnContext): TenantDetails { + // First try to extract tenant ID from activity recipient - use tenantId (camelCase) + const tenantId = + (context.activity.recipient as any)?.tenantId || + process.env.connections__serviceConnection__settings__tenantId || + "sample-tenant"; + + console.log( + `🏢 Tenant ID: ${tenantId} (from ${ + (context.activity.recipient as any)?.tenantId + ? "activity.recipient.tenantId" + : "environment/fallback" + })` + ); + + return { tenantId: tenantId }; +} diff --git a/nodejs/devin/tsconfig.json b/nodejs/devin/tsconfig.json new file mode 100644 index 00000000..0e188450 --- /dev/null +++ b/nodejs/devin/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "incremental": true, + "lib": ["ES2021"], + "target": "es2019", + "module": "commonjs", + "declaration": true, + "sourceMap": true, + "composite": true, + "strict": true, + "moduleResolution": "node", + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "rootDir": "src", + "outDir": "dist", + "tsBuildInfoFile": "dist/.tsbuildinfo" + } +} \ No newline at end of file From 6c8a2b65b0f6581381178c0ee879892a2a5b18c6 Mon Sep 17 00:00:00 2001 From: Walter Luna Date: Tue, 11 Nov 2025 11:23:29 +0000 Subject: [PATCH 02/13] remove terms & conditions check --- nodejs/devin/src/agent.ts | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/nodejs/devin/src/agent.ts b/nodejs/devin/src/agent.ts index 4ef30887..8c185fba 100644 --- a/nodejs/devin/src/agent.ts +++ b/nodejs/devin/src/agent.ts @@ -25,7 +25,6 @@ type ApplicationTurnState = TurnState; export class A365Agent extends AgentApplication { isApplicationInstalled: boolean = false; - termsAndConditionsAccepted: boolean = false; agentName = "Devin Agent"; constructor() { @@ -102,22 +101,6 @@ export class A365Agent extends AgentApplication { return; } - // TODO: Is this terms & conditions really required? - if (!this.termsAndConditionsAccepted) { - if (turnContext.activity.text?.trim().toLowerCase() === "i accept") { - this.termsAndConditionsAccepted = true; - await turnContext.sendActivity( - "Thank you for accepting the terms and conditions! How can I assist you today?" - ); - return; - } else { - await turnContext.sendActivity( - "Please accept the terms and conditions to proceed. Send 'I accept' to accept." - ); - return; - } - } - const userMessage = turnContext.activity.text?.trim() || ""; if (!userMessage) { From 19859201a14f958b7cfc0fabcec8dc25fad69727 Mon Sep 17 00:00:00 2001 From: Walter Luna Date: Tue, 11 Nov 2025 11:24:40 +0000 Subject: [PATCH 03/13] remove old comments --- nodejs/devin/src/agent.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/nodejs/devin/src/agent.ts b/nodejs/devin/src/agent.ts index 8c185fba..3be39ea6 100644 --- a/nodejs/devin/src/agent.ts +++ b/nodejs/devin/src/agent.ts @@ -60,8 +60,7 @@ export class A365Agent extends AgentApplication { context.activity.text ?? "Unknown text", ]); - await context.sendActivity(Activity.fromObject({ type: "typing" })); // TODO: figure out typing - + await context.sendActivity(Activity.fromObject({ type: "typing" })); await this.handleAgentMessageActivity( context, invokeAgentScope, From 0e72ee51880f0f847f54e28ee4d6b1a6bbffc4f4 Mon Sep 17 00:00:00 2001 From: Walter Luna Date: Tue, 11 Nov 2025 11:47:42 +0000 Subject: [PATCH 04/13] update directory structure --- nodejs/devin/{ => sample-agent}/.env.example | 0 nodejs/devin/{ => sample-agent}/README.md | 0 nodejs/devin/{ => sample-agent}/package.json | 0 nodejs/devin/{ => sample-agent}/src/agent.ts | 0 nodejs/devin/{ => sample-agent}/src/devin-client.ts | 0 nodejs/devin/{ => sample-agent}/src/index.ts | 0 nodejs/devin/{ => sample-agent}/src/types/devin-client.types.ts | 0 nodejs/devin/{ => sample-agent}/src/utils.ts | 0 nodejs/devin/{ => sample-agent}/tsconfig.json | 0 9 files changed, 0 insertions(+), 0 deletions(-) rename nodejs/devin/{ => sample-agent}/.env.example (100%) rename nodejs/devin/{ => sample-agent}/README.md (100%) rename nodejs/devin/{ => sample-agent}/package.json (100%) rename nodejs/devin/{ => sample-agent}/src/agent.ts (100%) rename nodejs/devin/{ => sample-agent}/src/devin-client.ts (100%) rename nodejs/devin/{ => sample-agent}/src/index.ts (100%) rename nodejs/devin/{ => sample-agent}/src/types/devin-client.types.ts (100%) rename nodejs/devin/{ => sample-agent}/src/utils.ts (100%) rename nodejs/devin/{ => sample-agent}/tsconfig.json (100%) diff --git a/nodejs/devin/.env.example b/nodejs/devin/sample-agent/.env.example similarity index 100% rename from nodejs/devin/.env.example rename to nodejs/devin/sample-agent/.env.example diff --git a/nodejs/devin/README.md b/nodejs/devin/sample-agent/README.md similarity index 100% rename from nodejs/devin/README.md rename to nodejs/devin/sample-agent/README.md diff --git a/nodejs/devin/package.json b/nodejs/devin/sample-agent/package.json similarity index 100% rename from nodejs/devin/package.json rename to nodejs/devin/sample-agent/package.json diff --git a/nodejs/devin/src/agent.ts b/nodejs/devin/sample-agent/src/agent.ts similarity index 100% rename from nodejs/devin/src/agent.ts rename to nodejs/devin/sample-agent/src/agent.ts diff --git a/nodejs/devin/src/devin-client.ts b/nodejs/devin/sample-agent/src/devin-client.ts similarity index 100% rename from nodejs/devin/src/devin-client.ts rename to nodejs/devin/sample-agent/src/devin-client.ts diff --git a/nodejs/devin/src/index.ts b/nodejs/devin/sample-agent/src/index.ts similarity index 100% rename from nodejs/devin/src/index.ts rename to nodejs/devin/sample-agent/src/index.ts diff --git a/nodejs/devin/src/types/devin-client.types.ts b/nodejs/devin/sample-agent/src/types/devin-client.types.ts similarity index 100% rename from nodejs/devin/src/types/devin-client.types.ts rename to nodejs/devin/sample-agent/src/types/devin-client.types.ts diff --git a/nodejs/devin/src/utils.ts b/nodejs/devin/sample-agent/src/utils.ts similarity index 100% rename from nodejs/devin/src/utils.ts rename to nodejs/devin/sample-agent/src/utils.ts diff --git a/nodejs/devin/tsconfig.json b/nodejs/devin/sample-agent/tsconfig.json similarity index 100% rename from nodejs/devin/tsconfig.json rename to nodejs/devin/sample-agent/tsconfig.json From 507403c2d939e34acecfa1121e8b86ef5452f2a0 Mon Sep 17 00:00:00 2001 From: Walter Luna Date: Tue, 11 Nov 2025 11:48:25 +0000 Subject: [PATCH 05/13] remove old references --- nodejs/devin/sample-agent/src/agent.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/nodejs/devin/sample-agent/src/agent.ts b/nodejs/devin/sample-agent/src/agent.ts index 3be39ea6..65414667 100644 --- a/nodejs/devin/sample-agent/src/agent.ts +++ b/nodejs/devin/sample-agent/src/agent.ts @@ -164,13 +164,11 @@ export class A365Agent extends AgentApplication { ): Promise { if (turnContext.activity.action === "add") { this.isApplicationInstalled = true; - this.termsAndConditionsAccepted = false; await turnContext.sendActivity( 'Thank you for hiring me! Looking forward to assisting you in your professional journey! Before I begin, could you please confirm that you accept the terms and conditions? Send "I accept" to accept.' ); } else if (turnContext.activity.action === "remove") { this.isApplicationInstalled = false; - this.termsAndConditionsAccepted = false; await turnContext.sendActivity( "Thank you for your time, I enjoyed working with you." ); From 552a317c3a984a1b095ebac5bc4c9b0d060c497d Mon Sep 17 00:00:00 2001 From: Walter Luna Date: Tue, 11 Nov 2025 12:11:45 +0000 Subject: [PATCH 06/13] Improve error handling on devin client --- nodejs/devin/sample-agent/src/devin-client.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/nodejs/devin/sample-agent/src/devin-client.ts b/nodejs/devin/sample-agent/src/devin-client.ts index f6eb334e..45f799c1 100644 --- a/nodejs/devin/sample-agent/src/devin-client.ts +++ b/nodejs/devin/sample-agent/src/devin-client.ts @@ -101,7 +101,11 @@ export class DevinClient implements Client { if (response.status !== 200) { console.error(`API call failed with status ${response.status}}`); console.error(`Error response: ${JSON.stringify(response)}}`); - break; // should we break or continue polling? + responseStream.emit( + "data", + "There was an error processing your request, please try again" + ); + break; } const data = (await response.json()) as DevinSessionResponse; From ddb7a30d50bc0141413b6215a22706de583dbf1d Mon Sep 17 00:00:00 2001 From: Walter Luna Date: Wed, 12 Nov 2025 12:43:23 +0000 Subject: [PATCH 07/13] changes from code review --- nodejs/devin/sample-agent/package.json | 6 +++--- nodejs/devin/sample-agent/src/agent.ts | 6 ++++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/nodejs/devin/sample-agent/package.json b/nodejs/devin/sample-agent/package.json index 64176bfa..8b84888e 100644 --- a/nodejs/devin/sample-agent/package.json +++ b/nodejs/devin/sample-agent/package.json @@ -10,7 +10,6 @@ "clean": "rimraf dist node_modules package-lock.json" }, "keywords": [], - "author": "walterluna@microsoft.com", "license": "ISC", "description": "", "dependencies": { @@ -18,10 +17,11 @@ "@microsoft/agents-a365-observability": "*", "@microsoft/agents-a365-runtime": "*", "@microsoft/agents-a365-tooling": "*", - "@microsoft/agents-hosting": "^1.0.15" + "@microsoft/agents-hosting": "^1.0.15", + "uuid": "^13.0.0" }, "devDependencies": { - "@microsoft/m365agentsplayground": "^0.2.18", + "@microsoft/m365agentsplayground": "^0.2.20", "nodemon": "^3.1.10", "rimraf": "^5.0.0", "ts-node": "^10.9.2", diff --git a/nodejs/devin/sample-agent/src/agent.ts b/nodejs/devin/sample-agent/src/agent.ts index 65414667..0c79abe0 100644 --- a/nodejs/devin/sample-agent/src/agent.ts +++ b/nodejs/devin/sample-agent/src/agent.ts @@ -15,6 +15,7 @@ import { TurnState, } from "@microsoft/agents-hosting"; import { Stream } from "stream"; +import { v4 as uuidv4 } from "uuid"; import { devinClient } from "./devin-client"; import { getAgentDetails, getTenantDetails } from "./utils"; @@ -36,6 +37,7 @@ export class A365Agent extends AgentApplication { // Increment count state let count = state.conversation.count ?? 0; state.conversation.count = ++count; + // Extract agent and tenant details from context const invokeAgentDetails = getAgentDetails(context); const tenantDetails = getTenantDetails(context); @@ -44,7 +46,7 @@ export class A365Agent extends AgentApplication { const baggageScope = new BaggageBuilder() .tenantId(tenantDetails.tenantId) .agentId(invokeAgentDetails.agentId) - .correlationId("73f5b5df-866e-4568-8ac7-ae0d36fc1de2") // TODO: ask, should a new uuid be generated each call? + .correlationId(uuidv4()) .agentName(invokeAgentDetails.agentName) .conversationId(context.activity.conversation?.id) .build(); @@ -165,7 +167,7 @@ export class A365Agent extends AgentApplication { if (turnContext.activity.action === "add") { this.isApplicationInstalled = true; await turnContext.sendActivity( - 'Thank you for hiring me! Looking forward to assisting you in your professional journey! Before I begin, could you please confirm that you accept the terms and conditions? Send "I accept" to accept.' + "Thank you for hiring me! Looking forward to assisting you in your professional journey!" ); } else if (turnContext.activity.action === "remove") { this.isApplicationInstalled = false; From e6e754df3dfda5e9391a8ac6ced131a70376d2de Mon Sep 17 00:00:00 2001 From: Walter Luna Date: Wed, 12 Nov 2025 16:49:42 +0000 Subject: [PATCH 08/13] Update nodejs/devin/sample-agent/src/agent.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- nodejs/devin/sample-agent/src/agent.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nodejs/devin/sample-agent/src/agent.ts b/nodejs/devin/sample-agent/src/agent.ts index 0c79abe0..c8c6f5a4 100644 --- a/nodejs/devin/sample-agent/src/agent.ts +++ b/nodejs/devin/sample-agent/src/agent.ts @@ -116,7 +116,7 @@ export class A365Agent extends AgentApplication { operationName: InferenceOperationType.CHAT, model: "claude-3-7-sonnet-20250219", providerName: "cognition-ai", - inputTokens: Math.ceil(userMessage.length / 4), // Rough estimate, + inputTokens: Math.ceil(userMessage.length / 4), // Rough estimate responseId: `resp-${Date.now()}`, outputTokens: 0, // Will be updated after response, finishReasons: undefined, From 15a0862a23cedd6b017e270eb92ad5189219cfb0 Mon Sep 17 00:00:00 2001 From: Walter Luna Date: Wed, 12 Nov 2025 16:49:56 +0000 Subject: [PATCH 09/13] Update nodejs/devin/sample-agent/src/devin-client.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- nodejs/devin/sample-agent/src/devin-client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nodejs/devin/sample-agent/src/devin-client.ts b/nodejs/devin/sample-agent/src/devin-client.ts index 45f799c1..6612804d 100644 --- a/nodejs/devin/sample-agent/src/devin-client.ts +++ b/nodejs/devin/sample-agent/src/devin-client.ts @@ -124,7 +124,7 @@ export class DevinClient implements Client { } } - console.debug("emiting close event"); + console.debug("emitting close event"); responseStream.emit("close"); } From 57506eea90bda94b85619c92ecf412b6e5eca07e Mon Sep 17 00:00:00 2001 From: Walter Luna Date: Wed, 12 Nov 2025 16:50:23 +0000 Subject: [PATCH 10/13] Update nodejs/devin/sample-agent/src/devin-client.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- nodejs/devin/sample-agent/src/devin-client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nodejs/devin/sample-agent/src/devin-client.ts b/nodejs/devin/sample-agent/src/devin-client.ts index 6612804d..ef254fb1 100644 --- a/nodejs/devin/sample-agent/src/devin-client.ts +++ b/nodejs/devin/sample-agent/src/devin-client.ts @@ -100,7 +100,7 @@ export class DevinClient implements Client { if (response.status !== 200) { console.error(`API call failed with status ${response.status}}`); - console.error(`Error response: ${JSON.stringify(response)}}`); + console.error(`Error response: ${JSON.stringify(response)}`); responseStream.emit( "data", "There was an error processing your request, please try again" From a669eec67e76ea8727f68b65cf2d2e0ab55156fc Mon Sep 17 00:00:00 2001 From: Walter Luna Date: Wed, 12 Nov 2025 16:50:40 +0000 Subject: [PATCH 11/13] Update nodejs/devin/sample-agent/src/agent.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- nodejs/devin/sample-agent/src/agent.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nodejs/devin/sample-agent/src/agent.ts b/nodejs/devin/sample-agent/src/agent.ts index c8c6f5a4..b88b5ddd 100644 --- a/nodejs/devin/sample-agent/src/agent.ts +++ b/nodejs/devin/sample-agent/src/agent.ts @@ -118,7 +118,7 @@ export class A365Agent extends AgentApplication { providerName: "cognition-ai", inputTokens: Math.ceil(userMessage.length / 4), // Rough estimate responseId: `resp-${Date.now()}`, - outputTokens: 0, // Will be updated after response, + outputTokens: 0, // Will be updated after response finishReasons: undefined, }; From e694d3f9f393923a36554eea95e05787e3f5f6c8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 12 Nov 2025 16:53:47 +0000 Subject: [PATCH 12/13] Initial plan From b069d33b1776e4a950e7ab9152524c6ea6ef1d7c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 12 Nov 2025 16:58:48 +0000 Subject: [PATCH 13/13] Add Microsoft copyright headers to all TypeScript files Co-authored-by: walterluna <22059865+walterluna@users.noreply.github.com> --- nodejs/devin/sample-agent/src/agent.ts | 3 +++ nodejs/devin/sample-agent/src/devin-client.ts | 3 +++ nodejs/devin/sample-agent/src/index.ts | 3 +++ nodejs/devin/sample-agent/src/types/devin-client.types.ts | 3 +++ nodejs/devin/sample-agent/src/utils.ts | 3 +++ 5 files changed, 15 insertions(+) diff --git a/nodejs/devin/sample-agent/src/agent.ts b/nodejs/devin/sample-agent/src/agent.ts index b88b5ddd..f9b73ac1 100644 --- a/nodejs/devin/sample-agent/src/agent.ts +++ b/nodejs/devin/sample-agent/src/agent.ts @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + import { AgentDetails, BaggageBuilder, diff --git a/nodejs/devin/sample-agent/src/devin-client.ts b/nodejs/devin/sample-agent/src/devin-client.ts index ef254fb1..563332a9 100644 --- a/nodejs/devin/sample-agent/src/devin-client.ts +++ b/nodejs/devin/sample-agent/src/devin-client.ts @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + import Stream from "stream"; import { DevinCreateSessionResponse, diff --git a/nodejs/devin/sample-agent/src/index.ts b/nodejs/devin/sample-agent/src/index.ts index 3b2ba015..a6c58093 100644 --- a/nodejs/devin/sample-agent/src/index.ts +++ b/nodejs/devin/sample-agent/src/index.ts @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + import { AuthConfiguration, authorizeJWT, diff --git a/nodejs/devin/sample-agent/src/types/devin-client.types.ts b/nodejs/devin/sample-agent/src/types/devin-client.types.ts index 7a615b06..e5c3135c 100644 --- a/nodejs/devin/sample-agent/src/types/devin-client.types.ts +++ b/nodejs/devin/sample-agent/src/types/devin-client.types.ts @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + export interface DevinCreateSessionResponse { session_id?: string; } diff --git a/nodejs/devin/sample-agent/src/utils.ts b/nodejs/devin/sample-agent/src/utils.ts index ab756b3e..1ba74519 100644 --- a/nodejs/devin/sample-agent/src/utils.ts +++ b/nodejs/devin/sample-agent/src/utils.ts @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + import { ExecutionType, InvokeAgentDetails,