Phase 1-2: Fix type conflicts and implement prompt submission
- Resolve duplicate DroneStatus enum (import from @gadget/api) - Fix IAiProvider interface conflict with DB→runtime mapper - Add callId to ToolCallMessage and ChatToolCallSchema - Fix ChatTurnStats schema field name (thinkingTokenCount) - Add provider/selectedModel to ChatSession interface and model - Implement CodeSession.onSubmitPrompt() to create ChatTurn and send work orders - Add drone/chat session tracking to CodeSession - Add unit tests for CodeSession (9 tests, all passing)
This commit is contained in:
parent
6591da3496
commit
8fe75b8c1c
@ -5,41 +5,41 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Phase 1: Fix Type Errors & Interface Conflicts
|
## Phase 1: Fix Type Errors & Interface Conflicts ✅ COMPLETE
|
||||||
|
|
||||||
### 1.1 Resolve Duplicate `DroneStatus` Enum
|
### 1.1 Resolve Duplicate `DroneStatus` Enum
|
||||||
- **File:** `gadget-drone/src/services/platform.ts`
|
- **File:** `gadget-drone/src/services/platform.ts`
|
||||||
- **Action:** Remove local `DroneStatus` enum, import from `@gadget/api`
|
- **Action:** Remove local `DroneStatus` enum, import from `@gadget/api`
|
||||||
- **Status:** ⬜ Pending
|
- **Status:** ✅ Complete
|
||||||
|
|
||||||
### 1.2 Resolve `IAiProvider` Interface Conflict
|
### 1.2 Resolve `IAiProvider` Interface Conflict
|
||||||
- **Files:**
|
- **Files:**
|
||||||
- `packages/api/src/interfaces/ai-provider.ts` (Mongoose document)
|
- `packages/api/src/interfaces/ai-provider.ts` (Mongoose document)
|
||||||
- `packages/ai/src/api.ts` (runtime config)
|
- `packages/ai/src/api.ts` (runtime config)
|
||||||
- **Action:** Create mapper in `gadget-drone/src/services/ai.ts` to convert DB model → runtime config
|
- **Action:** Create mapper in `gadget-drone/src/services/ai.ts` to convert DB model → runtime config
|
||||||
- **Status:** ⬜ Pending
|
- **Status:** ✅ Complete
|
||||||
|
|
||||||
### 1.3 Fix `ToolCallMessage` Signature
|
### 1.3 Fix `ToolCallMessage` Signature
|
||||||
- **File:** `packages/api/src/messages/drone.ts:26-30`
|
- **File:** `packages/api/src/messages/drone.ts:26-30`
|
||||||
- **Issue:** Missing `callId` parameter required by `IChatToolCall`
|
- **Issue:** Missing `callId` parameter required by `IChatToolCall`
|
||||||
- **Action:** Add `callId: string` as first parameter
|
- **Action:** Add `callId: string` as first parameter
|
||||||
- **Status:** ⬜ Pending
|
- **Status:** ✅ Complete
|
||||||
|
|
||||||
### 1.4 Fix `ChatTurnStats` Schema Mismatch
|
### 1.4 Fix `ChatTurnStats` Schema Mismatch
|
||||||
- **File:** `gadget-code/src/models/chat-turn.ts:70-76`
|
- **File:** `gadget-code/src/models/chat-turn.ts:70-76`
|
||||||
- **Issue:** Schema uses `thinkingTokens`, interface uses `thinkingTokenCount`
|
- **Issue:** Schema uses `thinkingTokens`, interface uses `thinkingTokenCount`
|
||||||
- **Action:** Standardize on `thinkingTokenCount` in schema
|
- **Action:** Standardize on `thinkingTokenCount` in schema
|
||||||
- **Status:** ⬜ Pending
|
- **Status:** ✅ Complete
|
||||||
|
|
||||||
### 1.5 Fix `ChatToolCallSchema` Missing `callId`
|
### 1.5 Fix `ChatToolCallSchema` Missing `callId`
|
||||||
- **File:** `gadget-code/src/models/chat-turn.ts:31-36`
|
- **File:** `gadget-code/src/models/chat-turn.ts:31-36`
|
||||||
- **Issue:** Schema doesn't include required `callId` field
|
- **Issue:** Schema doesn't include required `callId` field
|
||||||
- **Action:** Add `callId: { type: String, required: true }` to schema
|
- **Action:** Add `callId: { type: String, required: true }` to schema
|
||||||
- **Status:** ⬜ Pending
|
- **Status:** ✅ Complete
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Phase 2: Implement Prompt Submission Flow
|
## Phase 2: Implement Prompt Submission Flow ✅ COMPLETE
|
||||||
|
|
||||||
### 2.1 Implement `CodeSession.onSubmitPrompt()`
|
### 2.1 Implement `CodeSession.onSubmitPrompt()`
|
||||||
- **File:** `gadget-code/src/lib/code-session.ts:58-60`
|
- **File:** `gadget-code/src/lib/code-session.ts:58-60`
|
||||||
@ -50,12 +50,23 @@
|
|||||||
- Emit `processWorkOrder` to drone
|
- Emit `processWorkOrder` to drone
|
||||||
- Update `ChatTurn` with drone acknowledgment
|
- Update `ChatTurn` with drone acknowledgment
|
||||||
- **Missing:** Track `selectedDrone`, `chatSession`, `project` in `CodeSession`
|
- **Missing:** Track `selectedDrone`, `chatSession`, `project` in `CodeSession`
|
||||||
- **Status:** ⬜ Pending
|
- **Status:** ✅ Complete
|
||||||
|
|
||||||
### 2.2 Add Drone Selection to `CodeSession`
|
### 2.2 Add Drone Selection to `CodeSession`
|
||||||
- **File:** `gadget-code/src/lib/code-session.ts`
|
- **File:** `gadget-code/src/lib/code-session.ts`
|
||||||
- **Action:** Add properties and methods to track selected drone, chat session, project
|
- **Action:** Add properties and methods to track selected drone, chat session, project
|
||||||
- **Status:** ⬜ Pending
|
- **Status:** ✅ Complete
|
||||||
|
|
||||||
|
### 2.3 Add `provider` and `selectedModel` to ChatSession
|
||||||
|
- **Files:**
|
||||||
|
- `packages/api/src/interfaces/chat-session.ts`
|
||||||
|
- `gadget-code/src/models/chat-session.ts`
|
||||||
|
- **Status:** ✅ Complete
|
||||||
|
|
||||||
|
### 2.4 Unit Tests for CodeSession
|
||||||
|
- **File:** `gadget-code/tests/code-session.test.ts`
|
||||||
|
- **Tests:** 9 tests covering prompt submission flow
|
||||||
|
- **Status:** ✅ Complete (all passing)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@ -2,20 +2,30 @@
|
|||||||
// Copyright (C) 2026 Robert Colbert <rob.colbert@openplatform.us>
|
// Copyright (C) 2026 Robert Colbert <rob.colbert@openplatform.us>
|
||||||
// All Rights Reserved
|
// All Rights Reserved
|
||||||
|
|
||||||
|
import { Types } from "mongoose";
|
||||||
import {
|
import {
|
||||||
GadgetSocket,
|
GadgetSocket,
|
||||||
SocketSession,
|
SocketSession,
|
||||||
SocketSessionType,
|
SocketSessionType,
|
||||||
} from "./socket-session";
|
} from "./socket-session";
|
||||||
import { IChatSession, IDroneRegistration, IProject, IUser } from "@gadget/api";
|
import {
|
||||||
|
IChatSession,
|
||||||
|
IDroneRegistration,
|
||||||
|
IProject,
|
||||||
|
IUser,
|
||||||
|
ChatTurnStatus,
|
||||||
|
} from "@gadget/api";
|
||||||
|
|
||||||
import SocketService from "../services/socket.ts";
|
import SocketService from "../services/socket.ts";
|
||||||
|
import { ChatTurn } from "../models/chat-turn.ts";
|
||||||
|
|
||||||
export class CodeSession extends SocketSession {
|
export class CodeSession extends SocketSession {
|
||||||
protected type: SocketSessionType = SocketSessionType.Code;
|
protected type: SocketSessionType = SocketSessionType.Code;
|
||||||
|
|
||||||
protected project: IProject | undefined;
|
protected project: IProject | undefined;
|
||||||
protected chatSession: IChatSession | undefined;
|
protected chatSession: IChatSession | undefined;
|
||||||
|
protected selectedDrone: IDroneRegistration | undefined;
|
||||||
|
protected currentTurnId: Types.ObjectId | undefined;
|
||||||
|
|
||||||
constructor(socket: GadgetSocket, user: IUser) {
|
constructor(socket: GadgetSocket, user: IUser) {
|
||||||
super(socket, user);
|
super(socket, user);
|
||||||
@ -28,6 +38,24 @@ export class CodeSession extends SocketSession {
|
|||||||
this.socket.on("submitPrompt", this.onSubmitPrompt.bind(this));
|
this.socket.on("submitPrompt", this.onSubmitPrompt.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the selected drone for this code session.
|
||||||
|
*/
|
||||||
|
setSelectedDrone(registration: IDroneRegistration): void {
|
||||||
|
this.selectedDrone = registration;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the active chat session and project for this code session.
|
||||||
|
*/
|
||||||
|
setChatSession(
|
||||||
|
chatSession: IChatSession,
|
||||||
|
project: IProject,
|
||||||
|
): void {
|
||||||
|
this.chatSession = chatSession;
|
||||||
|
this.project = project;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when the IDE sends a requestSessionLock event to lock a gadget-drone
|
* Called when the IDE sends a requestSessionLock event to lock a gadget-drone
|
||||||
* instance to this code session.
|
* instance to this code session.
|
||||||
@ -50,12 +78,90 @@ export class CodeSession extends SocketSession {
|
|||||||
project,
|
project,
|
||||||
chatSession,
|
chatSession,
|
||||||
(success: boolean, chatSessionId: string): void => {
|
(success: boolean, chatSessionId: string): void => {
|
||||||
|
if (success) {
|
||||||
|
this.selectedDrone = registration;
|
||||||
|
this.chatSession = chatSession;
|
||||||
|
this.project = project;
|
||||||
|
}
|
||||||
cb(success, chatSessionId);
|
cb(success, chatSessionId);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the IDE submits a prompt to be processed by the agent.
|
||||||
|
* Creates a ChatTurn document and sends a work order to the selected drone.
|
||||||
|
*/
|
||||||
async onSubmitPrompt(content: string): Promise<void> {
|
async onSubmitPrompt(content: string): Promise<void> {
|
||||||
this.log.debug("prompt received", { content });
|
if (!this.selectedDrone) {
|
||||||
|
this.log.warn("prompt rejected: no drone selected");
|
||||||
|
throw new Error("No drone selected");
|
||||||
|
}
|
||||||
|
if (!this.chatSession) {
|
||||||
|
this.log.warn("prompt rejected: no chat session active");
|
||||||
|
throw new Error("No chat session active");
|
||||||
|
}
|
||||||
|
if (!this.project) {
|
||||||
|
this.log.warn("prompt rejected: no project selected");
|
||||||
|
throw new Error("No project selected");
|
||||||
|
}
|
||||||
|
|
||||||
|
const droneSession = SocketService.getDroneSession(this.selectedDrone);
|
||||||
|
|
||||||
|
const turn = new ChatTurn({
|
||||||
|
createdAt: new Date(),
|
||||||
|
user: this.user._id,
|
||||||
|
project: this.project._id,
|
||||||
|
session: this.chatSession._id,
|
||||||
|
provider: this.chatSession.provider,
|
||||||
|
llm: this.chatSession.selectedModel,
|
||||||
|
mode: this.chatSession.mode,
|
||||||
|
status: ChatTurnStatus.Processing,
|
||||||
|
prompts: {
|
||||||
|
user: content,
|
||||||
|
system: undefined,
|
||||||
|
},
|
||||||
|
toolCalls: [],
|
||||||
|
subagents: [],
|
||||||
|
stats: {
|
||||||
|
toolCallCount: 0,
|
||||||
|
inputTokens: 0,
|
||||||
|
thinkingTokenCount: 0,
|
||||||
|
responseTokens: 0,
|
||||||
|
durationMs: 0,
|
||||||
|
durationLabel: "pending",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await turn.save();
|
||||||
|
this.currentTurnId = turn._id;
|
||||||
|
|
||||||
|
this.log.info("ChatTurn created", {
|
||||||
|
turnId: turn._id.toHexString(),
|
||||||
|
chatSessionId: this.chatSession._id.toHexString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
droneSession.socket.emit(
|
||||||
|
"processWorkOrder",
|
||||||
|
this.selectedDrone,
|
||||||
|
this.project,
|
||||||
|
this.chatSession,
|
||||||
|
turn,
|
||||||
|
(success: boolean, message?: string) => {
|
||||||
|
if (success) {
|
||||||
|
this.log.info("work order accepted by drone", {
|
||||||
|
turnId: turn._id.toHexString(),
|
||||||
|
message,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.log.error("work order rejected by drone", {
|
||||||
|
turnId: turn._id.toHexString(),
|
||||||
|
message,
|
||||||
|
});
|
||||||
|
turn.status = ChatTurnStatus.Error;
|
||||||
|
turn.response = message || "Drone rejected work order";
|
||||||
|
turn.save();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,6 +21,8 @@ export const ChatSessionSchema = new Schema<IChatSession>({
|
|||||||
default: ChatSessionMode.Build,
|
default: ChatSessionMode.Build,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
provider: { type: Schema.Types.ObjectId, required: true, ref: "AiProvider" },
|
||||||
|
selectedModel: { type: String, required: true },
|
||||||
stats: {
|
stats: {
|
||||||
turnCount: { type: Number, default: 0, required: true },
|
turnCount: { type: Number, default: 0, required: true },
|
||||||
toolCallCount: { type: Number, default: 0, required: true },
|
toolCallCount: { type: Number, default: 0, required: true },
|
||||||
|
|||||||
@ -29,6 +29,7 @@ export const ChatTurnStatsSchema = new Schema<IChatTurnStats>({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const ChatToolCallSchema = new Schema<IChatToolCall>({
|
export const ChatToolCallSchema = new Schema<IChatToolCall>({
|
||||||
|
callId: { type: String, required: true },
|
||||||
name: { type: String, required: true },
|
name: { type: String, required: true },
|
||||||
parameters: { type: String, required: false },
|
parameters: { type: String, required: false },
|
||||||
response: { type: String, required: false },
|
response: { type: String, required: false },
|
||||||
@ -69,7 +70,7 @@ export const ChatTurnSchema = new Schema<IChatTurn>({
|
|||||||
stats: {
|
stats: {
|
||||||
toolCallCount: { type: Number, default: 0, required: true },
|
toolCallCount: { type: Number, default: 0, required: true },
|
||||||
inputTokens: { type: Number, default: 0, required: true },
|
inputTokens: { type: Number, default: 0, required: true },
|
||||||
thinkingTokens: { type: Number, default: 0, required: true },
|
thinkingTokenCount: { type: Number, default: 0, required: true },
|
||||||
responseTokens: { type: Number, default: 0, required: true },
|
responseTokens: { type: Number, default: 0, required: true },
|
||||||
durationMs: { type: Number, default: 0, required: true },
|
durationMs: { type: Number, default: 0, required: true },
|
||||||
durationLabel: { type: String, default: "pending", required: true },
|
durationLabel: { type: String, default: "pending", required: true },
|
||||||
|
|||||||
209
gadget-code/tests/code-session.test.ts
Normal file
209
gadget-code/tests/code-session.test.ts
Normal file
@ -0,0 +1,209 @@
|
|||||||
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||||
|
import { Types } from 'mongoose';
|
||||||
|
import { CodeSession } from '../src/lib/code-session';
|
||||||
|
import { IChatSession, IProject, IUser, IDroneRegistration, ChatTurnStatus } from '@gadget/api';
|
||||||
|
import SocketService from '../src/services/socket';
|
||||||
|
import { ChatTurn } from '../src/models/chat-turn';
|
||||||
|
|
||||||
|
// Mock dependencies
|
||||||
|
vi.mock('../src/services/socket');
|
||||||
|
vi.mock('../src/models/chat-turn');
|
||||||
|
|
||||||
|
describe('CodeSession', () => {
|
||||||
|
let mockSocket: any;
|
||||||
|
let mockUser: IUser;
|
||||||
|
let mockDrone: IDroneRegistration;
|
||||||
|
let mockChatSession: IChatSession;
|
||||||
|
let mockProject: IProject;
|
||||||
|
let codeSession: CodeSession;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
|
||||||
|
mockSocket = {
|
||||||
|
id: 'test-socket-id',
|
||||||
|
on: vi.fn(),
|
||||||
|
emit: vi.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
mockUser = {
|
||||||
|
_id: new Types.ObjectId(),
|
||||||
|
email: 'test@example.com',
|
||||||
|
displayName: 'Test User',
|
||||||
|
} as IUser;
|
||||||
|
|
||||||
|
mockDrone = {
|
||||||
|
_id: new Types.ObjectId(),
|
||||||
|
hostname: 'test-host',
|
||||||
|
workspaceDir: '/test/workspace',
|
||||||
|
status: 'available',
|
||||||
|
} as IDroneRegistration;
|
||||||
|
|
||||||
|
mockChatSession = {
|
||||||
|
_id: new Types.ObjectId(),
|
||||||
|
name: 'Test Session',
|
||||||
|
mode: 'build',
|
||||||
|
provider: new Types.ObjectId(),
|
||||||
|
selectedModel: 'llama3.1',
|
||||||
|
} as IChatSession;
|
||||||
|
|
||||||
|
mockProject = {
|
||||||
|
_id: new Types.ObjectId(),
|
||||||
|
slug: 'test-project',
|
||||||
|
name: 'Test Project',
|
||||||
|
} as IProject;
|
||||||
|
|
||||||
|
codeSession = new CodeSession(mockSocket, mockUser);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setSelectedDrone', () => {
|
||||||
|
it('should set the selected drone', () => {
|
||||||
|
codeSession.setSelectedDrone(mockDrone);
|
||||||
|
// Can't directly access private property, but we can verify through behavior
|
||||||
|
expect(() => codeSession.setSelectedDrone(mockDrone)).not.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setChatSession', () => {
|
||||||
|
it('should set the chat session and project', () => {
|
||||||
|
codeSession.setChatSession(mockChatSession, mockProject);
|
||||||
|
expect(() => codeSession.setChatSession(mockChatSession, mockProject)).not.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onSubmitPrompt', () => {
|
||||||
|
it('should throw error if no drone is selected', async () => {
|
||||||
|
codeSession.setChatSession(mockChatSession, mockProject);
|
||||||
|
|
||||||
|
await expect(codeSession.onSubmitPrompt('test prompt'))
|
||||||
|
.rejects.toThrow('No drone selected');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error if no chat session is active', async () => {
|
||||||
|
codeSession.setSelectedDrone(mockDrone);
|
||||||
|
|
||||||
|
await expect(codeSession.onSubmitPrompt('test prompt'))
|
||||||
|
.rejects.toThrow('No chat session active');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error if no project is selected', async () => {
|
||||||
|
codeSession.setSelectedDrone(mockDrone);
|
||||||
|
codeSession.setChatSession(mockChatSession, undefined as any);
|
||||||
|
|
||||||
|
await expect(codeSession.onSubmitPrompt('test prompt'))
|
||||||
|
.rejects.toThrow('No project selected');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create a ChatTurn and emit processWorkOrder to drone', async () => {
|
||||||
|
codeSession.setSelectedDrone(mockDrone);
|
||||||
|
codeSession.setChatSession(mockChatSession, mockProject);
|
||||||
|
|
||||||
|
const mockTurn = {
|
||||||
|
_id: new Types.ObjectId(),
|
||||||
|
save: vi.fn().mockResolvedValue(undefined),
|
||||||
|
};
|
||||||
|
vi.mocked(ChatTurn).mockImplementation(function() {
|
||||||
|
return mockTurn as any;
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockDroneSession = {
|
||||||
|
socket: {
|
||||||
|
emit: vi.fn(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
vi.mocked(SocketService.getDroneSession).mockReturnValue(mockDroneSession as any);
|
||||||
|
|
||||||
|
await codeSession.onSubmitPrompt('test prompt');
|
||||||
|
|
||||||
|
expect(ChatTurn).toHaveBeenCalledWith(expect.objectContaining({
|
||||||
|
user: mockUser._id,
|
||||||
|
project: mockProject._id,
|
||||||
|
session: mockChatSession._id,
|
||||||
|
provider: mockChatSession.provider,
|
||||||
|
llm: mockChatSession.selectedModel,
|
||||||
|
status: ChatTurnStatus.Processing,
|
||||||
|
prompts: {
|
||||||
|
user: 'test prompt',
|
||||||
|
system: undefined,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
expect(mockTurn.save).toHaveBeenCalled();
|
||||||
|
expect(mockDroneSession.socket.emit).toHaveBeenCalledWith(
|
||||||
|
'processWorkOrder',
|
||||||
|
mockDrone,
|
||||||
|
mockProject,
|
||||||
|
mockChatSession,
|
||||||
|
mockTurn,
|
||||||
|
expect.any(Function)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update ChatTurn to Error status if drone rejects work order', async () => {
|
||||||
|
codeSession.setSelectedDrone(mockDrone);
|
||||||
|
codeSession.setChatSession(mockChatSession, mockProject);
|
||||||
|
|
||||||
|
const mockTurn = {
|
||||||
|
_id: new Types.ObjectId(),
|
||||||
|
status: ChatTurnStatus.Processing,
|
||||||
|
response: '',
|
||||||
|
save: vi.fn().mockResolvedValue(undefined),
|
||||||
|
};
|
||||||
|
vi.mocked(ChatTurn).mockImplementation(function() {
|
||||||
|
return mockTurn as any;
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockDroneSession = {
|
||||||
|
socket: {
|
||||||
|
emit: vi.fn((event, ...args) => {
|
||||||
|
const callback = args[args.length - 1];
|
||||||
|
callback(false, 'Drone is busy');
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
vi.mocked(SocketService.getDroneSession).mockReturnValue(mockDroneSession as any);
|
||||||
|
|
||||||
|
await codeSession.onSubmitPrompt('test prompt');
|
||||||
|
|
||||||
|
expect(mockTurn.status).toBe(ChatTurnStatus.Error);
|
||||||
|
expect(mockTurn.response).toBe('Drone is busy');
|
||||||
|
expect(mockTurn.save).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onRequestSessionLock', () => {
|
||||||
|
it('should set selected drone, chat session, and project on success', () => {
|
||||||
|
const mockDroneSession = {
|
||||||
|
socket: {
|
||||||
|
emit: vi.fn((event, ...args) => {
|
||||||
|
const callback = args[args.length - 1];
|
||||||
|
callback(true, mockChatSession._id.toHexString());
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
vi.mocked(SocketService.getDroneSession).mockReturnValue(mockDroneSession as any);
|
||||||
|
|
||||||
|
const callback = vi.fn();
|
||||||
|
codeSession.onRequestSessionLock(mockDrone, mockProject, mockChatSession, callback);
|
||||||
|
|
||||||
|
expect(callback).toHaveBeenCalledWith(true, mockChatSession._id.toHexString());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not set session data on failure', () => {
|
||||||
|
const mockDroneSession = {
|
||||||
|
socket: {
|
||||||
|
emit: vi.fn((event, ...args) => {
|
||||||
|
const callback = args[args.length - 1];
|
||||||
|
callback(false, '');
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
vi.mocked(SocketService.getDroneSession).mockReturnValue(mockDroneSession as any);
|
||||||
|
|
||||||
|
const callback = vi.fn();
|
||||||
|
codeSession.onRequestSessionLock(mockDrone, mockProject, mockChatSession, callback);
|
||||||
|
|
||||||
|
expect(callback).toHaveBeenCalledWith(false, '');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -10,10 +10,8 @@ import { input as inqInput, password as inqPassword } from "@inquirer/prompts";
|
|||||||
|
|
||||||
import AgentService, { IAgentWorkOrder } from "./services/agent.ts";
|
import AgentService, { IAgentWorkOrder } from "./services/agent.ts";
|
||||||
import AiService from "./services/ai.ts";
|
import AiService from "./services/ai.ts";
|
||||||
import PlatformService, {
|
import PlatformService, { PlatformRegistration } from "./services/platform.ts";
|
||||||
DroneStatus,
|
import { DroneStatus } from "@gadget/api";
|
||||||
PlatformRegistration,
|
|
||||||
} from "./services/platform.ts";
|
|
||||||
|
|
||||||
import { GadgetProcess } from "./lib/process.ts";
|
import { GadgetProcess } from "./lib/process.ts";
|
||||||
import {
|
import {
|
||||||
|
|||||||
@ -2,9 +2,9 @@
|
|||||||
// Copyright (C) 2026 Rob Colbert <rob.colbert@openplatform.us>
|
// Copyright (C) 2026 Rob Colbert <rob.colbert@openplatform.us>
|
||||||
// Licensed under the Apache License, Version 2.0
|
// Licensed under the Apache License, Version 2.0
|
||||||
|
|
||||||
|
import { Types } from "mongoose";
|
||||||
import {
|
import {
|
||||||
IAiChatOptions,
|
IAiChatOptions,
|
||||||
type IAiProvider,
|
|
||||||
type IContextChatMessage,
|
type IContextChatMessage,
|
||||||
} from "@gadget/ai";
|
} from "@gadget/ai";
|
||||||
import { IChatSession, IChatTurn, IProject, IUser } from "@gadget/api";
|
import { IChatSession, IChatTurn, IProject, IUser } from "@gadget/api";
|
||||||
@ -50,17 +50,6 @@ class AgentService extends GadgetService {
|
|||||||
return "[all tool calls are stubbed out]";
|
return "[all tool calls are stubbed out]";
|
||||||
}
|
}
|
||||||
|
|
||||||
const modelConfig = {
|
|
||||||
provider: turn.provider,
|
|
||||||
modelId: turn.llm,
|
|
||||||
params: {
|
|
||||||
reasoning: false,
|
|
||||||
temperature: 0.8,
|
|
||||||
topP: 0.9,
|
|
||||||
topK: 40,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const context = this.buildSessionContext(workOrder);
|
const context = this.buildSessionContext(workOrder);
|
||||||
const chatOptions: IAiChatOptions = {
|
const chatOptions: IAiChatOptions = {
|
||||||
systemPrompt: turn.prompts.system,
|
systemPrompt: turn.prompts.system,
|
||||||
@ -72,7 +61,15 @@ class AgentService extends GadgetService {
|
|||||||
do {
|
do {
|
||||||
const response = await AiService.chat(
|
const response = await AiService.chat(
|
||||||
turn.provider,
|
turn.provider,
|
||||||
modelConfig,
|
{
|
||||||
|
modelId: turn.llm,
|
||||||
|
params: {
|
||||||
|
reasoning: false,
|
||||||
|
temperature: 0.8,
|
||||||
|
topP: 0.9,
|
||||||
|
topK: 40,
|
||||||
|
},
|
||||||
|
},
|
||||||
chatOptions,
|
chatOptions,
|
||||||
);
|
);
|
||||||
keepProcessing = (response.toolCalls?.length ?? 0) > 0;
|
keepProcessing = (response.toolCalls?.length ?? 0) > 0;
|
||||||
@ -99,7 +96,13 @@ class AgentService extends GadgetService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
buildSessionContext(workOrder: IAgentWorkOrder): IContextChatMessage[] {
|
buildSessionContext(workOrder: IAgentWorkOrder): IContextChatMessage[] {
|
||||||
const user: IUser = workOrder.turn.session.user as IUser;
|
const session = workOrder.turn.session;
|
||||||
|
if (session instanceof Types.ObjectId || !session.user) {
|
||||||
|
throw new Error(
|
||||||
|
"ChatSession must be populated with user data",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const user: IUser = session.user as IUser;
|
||||||
const messages: IContextChatMessage[] = [];
|
const messages: IContextChatMessage[] = [];
|
||||||
|
|
||||||
for (const turn of workOrder.context) {
|
for (const turn of workOrder.context) {
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
// Copyright (C) 2026 Rob Colbert <rob.colbert@openplatform.us>
|
// Copyright (C) 2026 Rob Colbert <rob.colbert@openplatform.us>
|
||||||
// Licensed under the Apache License, Version 2.0
|
// Licensed under the Apache License, Version 2.0
|
||||||
|
|
||||||
|
import { Types } from "mongoose";
|
||||||
import { GadgetService } from "../lib/service.ts";
|
import { GadgetService } from "../lib/service.ts";
|
||||||
import {
|
import {
|
||||||
type IAiChatOptions,
|
type IAiChatOptions,
|
||||||
@ -9,10 +10,25 @@ import {
|
|||||||
type IAiGenerateOptions,
|
type IAiGenerateOptions,
|
||||||
type IAiGenerateResponse,
|
type IAiGenerateResponse,
|
||||||
type IAiModelConfig,
|
type IAiModelConfig,
|
||||||
type IAiProvider,
|
type IAiProvider as AiProviderConfig,
|
||||||
type IAiResponseStreamFn,
|
type IAiResponseStreamFn,
|
||||||
createAiApi,
|
createAiApi,
|
||||||
} from "@gadget/ai";
|
} from "@gadget/ai";
|
||||||
|
import { IAiProvider as DbAiProvider } from "@gadget/api";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drone-specific model config that accepts the database provider type.
|
||||||
|
*/
|
||||||
|
export interface IDroneModelConfig {
|
||||||
|
provider: DbAiProvider | Types.ObjectId;
|
||||||
|
modelId: string;
|
||||||
|
params: {
|
||||||
|
reasoning: boolean;
|
||||||
|
temperature: number;
|
||||||
|
topP: number;
|
||||||
|
topK: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An abstraction of the backend AI APIs (Ollama, OpenAI) that provides one
|
* An abstraction of the backend AI APIs (Ollama, OpenAI) that provides one
|
||||||
@ -40,54 +56,81 @@ class AiService extends GadgetService {
|
|||||||
this.log.info("stopped");
|
this.log.info("stopped");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a database IAiProvider document to a runtime IAiProvider config.
|
||||||
|
* The DB model uses `apiType` and extends Mongoose Document, while the runtime
|
||||||
|
* config uses `sdk` and is a plain object.
|
||||||
|
*/
|
||||||
|
mapDbProviderToConfig(
|
||||||
|
provider: DbAiProvider | Types.ObjectId,
|
||||||
|
): AiProviderConfig {
|
||||||
|
if (provider instanceof Types.ObjectId) {
|
||||||
|
throw new Error(
|
||||||
|
"Provider must be populated, not an ObjectId reference",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
_id: provider._id.toHexString(),
|
||||||
|
name: provider.name,
|
||||||
|
sdk: provider.apiType, // map apiType → sdk
|
||||||
|
baseUrl: provider.baseUrl,
|
||||||
|
apiKey: provider.apiKey,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Query the list of models available from the provider, then queries the
|
* Query the list of models available from the provider, then queries the
|
||||||
* models for their individual capabilities. The results are cached in the Gadget
|
* models for their individual capabilities. The results are cached in the Gadget
|
||||||
*/
|
*/
|
||||||
async discovery(provider: IAiProvider): Promise<void> {
|
async discovery(provider: DbAiProvider | Types.ObjectId): Promise<void> {
|
||||||
|
const config = this.mapDbProviderToConfig(provider);
|
||||||
this.log.info("discovering provider model list", {
|
this.log.info("discovering provider model list", {
|
||||||
name: provider.name,
|
name: config.name,
|
||||||
sdk: provider.sdk,
|
sdk: config.sdk,
|
||||||
});
|
});
|
||||||
const api = this.getApi(provider);
|
const api = this.getApi(config);
|
||||||
const response = await api.listModels();
|
const response = await api.listModels();
|
||||||
this.log.debug("listModels response", { response });
|
this.log.debug("listModels response", { response });
|
||||||
}
|
}
|
||||||
|
|
||||||
async generate(
|
async generate(
|
||||||
provider: IAiProvider,
|
provider: DbAiProvider | Types.ObjectId,
|
||||||
model: IAiModelConfig,
|
model: Omit<IAiModelConfig, "provider">,
|
||||||
options: IAiGenerateOptions,
|
options: IAiGenerateOptions,
|
||||||
streamCallback?: IAiResponseStreamFn,
|
streamCallback?: IAiResponseStreamFn,
|
||||||
): Promise<IAiGenerateResponse> {
|
): Promise<IAiGenerateResponse> {
|
||||||
|
const config = this.mapDbProviderToConfig(provider);
|
||||||
this.log.info("calling provider to generate a response", {
|
this.log.info("calling provider to generate a response", {
|
||||||
name: provider.name,
|
name: config.name,
|
||||||
sdk: provider.sdk,
|
sdk: config.sdk,
|
||||||
haveStreamCallback: !!streamCallback,
|
haveStreamCallback: !!streamCallback,
|
||||||
options,
|
options,
|
||||||
});
|
});
|
||||||
const api = this.getApi(provider);
|
const api = this.getApi(config);
|
||||||
return api.generate(model, options, streamCallback);
|
const modelConfig: IAiModelConfig = { ...model, provider: config };
|
||||||
|
return api.generate(modelConfig, options, streamCallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
async chat(
|
async chat(
|
||||||
provider: IAiProvider,
|
provider: DbAiProvider | Types.ObjectId,
|
||||||
model: IAiModelConfig,
|
model: Omit<IAiModelConfig, "provider">,
|
||||||
options: IAiChatOptions,
|
options: IAiChatOptions,
|
||||||
streamCallback?: IAiResponseStreamFn,
|
streamCallback?: IAiResponseStreamFn,
|
||||||
): Promise<IAiChatResponse> {
|
): Promise<IAiChatResponse> {
|
||||||
|
const config = this.mapDbProviderToConfig(provider);
|
||||||
this.log.info("calling provider to process chat", {
|
this.log.info("calling provider to process chat", {
|
||||||
provider: provider.name,
|
provider: config.name,
|
||||||
sdk: provider.sdk,
|
sdk: config.sdk,
|
||||||
haveStreamCallback: !!streamCallback,
|
haveStreamCallback: !!streamCallback,
|
||||||
model,
|
model,
|
||||||
options,
|
options,
|
||||||
});
|
});
|
||||||
const api = this.getApi(provider);
|
const api = this.getApi(config);
|
||||||
return await api.chat(model, options, streamCallback);
|
const modelConfig: IAiModelConfig = { ...model, provider: config };
|
||||||
|
return await api.chat(modelConfig, options, streamCallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
getApi(provider: IAiProvider) {
|
getApi(provider: AiProviderConfig) {
|
||||||
return createAiApi(provider, this.log);
|
return createAiApi(provider, this.log);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,13 +9,7 @@ import path from "node:path";
|
|||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
|
|
||||||
import { GadgetService } from "../lib/service.ts";
|
import { GadgetService } from "../lib/service.ts";
|
||||||
|
import { DroneStatus } from "@gadget/api";
|
||||||
export enum DroneStatus {
|
|
||||||
Starting = "starting",
|
|
||||||
Available = "available",
|
|
||||||
Busy = "busy",
|
|
||||||
Offline = "offline",
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PlatformRegistration {
|
export interface PlatformRegistration {
|
||||||
_id: string; // your drone's registration ID, channel, and queue
|
_id: string; // your drone's registration ID, channel, and queue
|
||||||
|
|||||||
@ -26,6 +26,8 @@ export interface IChatSession extends Document {
|
|||||||
project: IProject | Types.ObjectId;
|
project: IProject | Types.ObjectId;
|
||||||
name: string;
|
name: string;
|
||||||
mode: ChatSessionMode;
|
mode: ChatSessionMode;
|
||||||
|
provider: Types.ObjectId;
|
||||||
|
selectedModel: string;
|
||||||
stats: {
|
stats: {
|
||||||
turnCount: number;
|
turnCount: number;
|
||||||
toolCallCount: number;
|
toolCallCount: number;
|
||||||
|
|||||||
@ -24,6 +24,7 @@ export type ThinkingMessage = (content: string) => void;
|
|||||||
export type ResponseMessage = (content: string) => void;
|
export type ResponseMessage = (content: string) => void;
|
||||||
|
|
||||||
export type ToolCallMessage = (
|
export type ToolCallMessage = (
|
||||||
|
callId: string,
|
||||||
name: string,
|
name: string,
|
||||||
params: string,
|
params: string,
|
||||||
response: string,
|
response: string,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user