Version 0.1.0 (first commit) -- see README.md for details
This commit is contained in:
commit
58659b3e33
4 changed files with 265 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
node_modules
|
||||||
|
bun.lock
|
78
README.md
Normal file
78
README.md
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
# theatrics
|
||||||
|
|
||||||
|
*JS Actor System with first-class support for AI Agents and Assistants*
|
||||||
|
|
||||||
|
## about
|
||||||
|
|
||||||
|
theatrics is a lightweight JavaScript actor system designed for building interactive, real-time applications with strong support for AI agents and assistants. It relies solely on the Bun runtime and the [framerock](https://git.daemons.my/dab/framerock) framework, which provides zero-dependency web server and WebSocket infrastructure.
|
||||||
|
|
||||||
|
## features
|
||||||
|
|
||||||
|
theatrics enables structured, modular interaction between actors through a clean message-passing model. It manages DOM setup, role definitions, and bidirectional communication between client and server.
|
||||||
|
|
||||||
|
| Feature | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| Role-based actor model | Each role represents a functional unit that can receive input and return results via defined cues |
|
||||||
|
| Real-time interaction | Clients can initiate cues and receive responses instantly through WebSocket transport |
|
||||||
|
| Dynamic UI rendering | The frontend automatically renders a table of role definitions with interactive buttons to trigger actions |
|
||||||
|
|
||||||
|
## AI-Ready
|
||||||
|
|
||||||
|
theatrics is specifically optimized for integration with AI tools and agent workflows. It minimizes context overhead by exposing only the relevant code and message structure to language models, enabling prompt engineering and iterative behavior refinement without infrastructure noise.
|
||||||
|
|
||||||
|
| Benefit | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| Minimal context pollution | LLMs see only role logic and message patterns, not server routing or boilerplate |
|
||||||
|
| Direct prompt compatibility | Role functions can be directly evaluated or generated as code by AI tools |
|
||||||
|
| Safe and structured execution | All input and output are serialized as JSON, avoiding unsafe string parsing or injection risks |
|
||||||
|
|
||||||
|
## when to use
|
||||||
|
|
||||||
|
- when building agent-based applications where multiple AI-driven actors need to collaborate in real time
|
||||||
|
- when prototyping or refining agent behaviors using AI tools with minimal setup or abstraction
|
||||||
|
- when you want a clean, portable foundation that supports both human and AI-driven interaction with your app
|
||||||
|
|
||||||
|
## getting started
|
||||||
|
|
||||||
|
Install theatrics via Bun:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun add 'git+https://git.daemons.my/dab/theatrics.git'
|
||||||
|
```
|
||||||
|
|
||||||
|
Create a file named `your-app-entrypoint.js` with the following content:
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { async_stage } from 'theatrics'
|
||||||
|
|
||||||
|
const roles = {
|
||||||
|
'say-hello': () => `Hello!`,
|
||||||
|
}
|
||||||
|
|
||||||
|
await async_stage(roles, { framerock_config: { page_title: 'theatrics | minimal example' } })
|
||||||
|
```
|
||||||
|
|
||||||
|
Run the app:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run ./your-app-entrypoint.js
|
||||||
|
```
|
||||||
|
|
||||||
|
Your app will be visible at `http://127.0.0.1:8800`. You can customize the hostname and port using the `framerock_config` object.
|
||||||
|
|
||||||
|
## codebase overview
|
||||||
|
|
||||||
|
| file | purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `src/index.js` | Implements the core actor system, including role handling, cue initiation, result delivery, DOM setup, and integration with framerock's transport layer |
|
||||||
|
|
||||||
|
## why theatrics
|
||||||
|
|
||||||
|
- simplifies the development of complex, interactive agent systems by abstracting communication and state management
|
||||||
|
- enables rapid iteration when paired with AI tools for generating or refining role logic
|
||||||
|
- provides a transparent and predictable interface for both developers and AI agents to interact with the system
|
||||||
|
|
||||||
|
## changelog
|
||||||
|
|
||||||
|
- **Version 0.1.0**
|
||||||
|
- initial release
|
9
package.json
Normal file
9
package.json
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"name": "theatrics",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"type": "module",
|
||||||
|
"main": "src/index.js",
|
||||||
|
"dependencies": {
|
||||||
|
"framerock": "git+https://git.daemons.my/dab/framerock.git#7a7fbfa42f4f098f6b7242e47185497ba04bce9e"
|
||||||
|
}
|
||||||
|
}
|
176
src/index.js
Normal file
176
src/index.js
Normal file
|
@ -0,0 +1,176 @@
|
||||||
|
import { isAsyncFunction } from 'node:util/types'
|
||||||
|
import { async_run } from 'framerock'
|
||||||
|
|
||||||
|
function bytesToBase64 (bytes) {
|
||||||
|
return btoa(Array.from(bytes, function (byte) {return String.fromCodePoint(byte);}).join(''))
|
||||||
|
}
|
||||||
|
|
||||||
|
const async_stage = async function (role_defs, { framerock_config }) {
|
||||||
|
|
||||||
|
const jsbuild_app_frontend = async function () {
|
||||||
|
const page_params = { role_keys: [ ...Object.keys(role_defs) ] }
|
||||||
|
const str_b64params = bytesToBase64(new TextEncoder().encode(JSON.stringify(page_params)))
|
||||||
|
return `
|
||||||
|
function base64ToBytes (base64) {
|
||||||
|
return Uint8Array.from(atob(base64), function (m) {return m.codePointAt(0);})
|
||||||
|
}
|
||||||
|
|
||||||
|
const page_params = JSON.parse(new TextDecoder().decode(base64ToBytes("${str_b64params}")))
|
||||||
|
|
||||||
|
const result_handlers = new Map()
|
||||||
|
|
||||||
|
const setup_dom = function () {
|
||||||
|
|
||||||
|
document.head.insertAdjacentHTML('beforeend', '<style type="text/css">html, body{ height: 100%; width: 100%; margin: 0px; padding: 0px; overflow: hidden;}</style>')
|
||||||
|
const elem_root = document.createElement('div')
|
||||||
|
elem_root.style = 'height: 100%; width: 100%; font-family: sans-serif; position: relative; font-size: 16px;'
|
||||||
|
|
||||||
|
const elem_topbar = document.createElement('div')
|
||||||
|
elem_topbar.style = 'height: 100px; background-color: #333; color: #FFF; display: flex; align-items: center; overflow: hidden;'
|
||||||
|
|
||||||
|
const elem_title = document.createElement('div')
|
||||||
|
elem_title.style = 'font-size: 32px; font-weight: bold; user-select: none; margin-left: 32px;'
|
||||||
|
elem_title.textContent = 'theatrics'
|
||||||
|
|
||||||
|
const elem_view = document.createElement('div')
|
||||||
|
elem_view.style = 'position: absolute; top: 100px; left: 0px; right: 0px; bottom: 0px; background-color: #000; color: #FFF; overflow: auto;'
|
||||||
|
|
||||||
|
const elems_roledefs = page_params.role_keys.map((role_key) => {
|
||||||
|
const elem_tr = document.createElement('tr')
|
||||||
|
|
||||||
|
const t1 = document.createElement('td')
|
||||||
|
const t2 = document.createElement('td')
|
||||||
|
const t3 = document.createElement('td')
|
||||||
|
|
||||||
|
t1.textContent = role_key
|
||||||
|
|
||||||
|
const elem_results = document.createElement('div')
|
||||||
|
elem_results.style = 'max-height: 200px; overflow: auto; font-family: monospace; font-size: 14px;'
|
||||||
|
t3.appendChild(elem_results)
|
||||||
|
|
||||||
|
const elem_btninitcue = document.createElement('button')
|
||||||
|
elem_btninitcue.textContent = 'initiate cue'
|
||||||
|
const func_click_rolecue = () => {
|
||||||
|
elem_btninitcue.disabled = true
|
||||||
|
initiate_cue_handle_result(role_key, { foo: 'bar' }, (result_obj) => {
|
||||||
|
|
||||||
|
//console.info({ result_obj })
|
||||||
|
|
||||||
|
elem_btninitcue.disabled = false
|
||||||
|
|
||||||
|
const elem_resultrow = document.createElement('div')
|
||||||
|
elem_resultrow.style = 'border: 1px solid blue; padding: 10px; box-sizing: border-box;'
|
||||||
|
elem_resultrow.textContent = JSON.stringify(result_obj)
|
||||||
|
elem_results.appendChild(elem_resultrow)
|
||||||
|
|
||||||
|
return
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
elem_btninitcue.addEventListener('click', func_click_rolecue)
|
||||||
|
t2.appendChild(elem_btninitcue)
|
||||||
|
|
||||||
|
elem_tr.appendChild(t1)
|
||||||
|
elem_tr.appendChild(t2)
|
||||||
|
elem_tr.appendChild(t3)
|
||||||
|
|
||||||
|
return elem_tr
|
||||||
|
})
|
||||||
|
|
||||||
|
elem_topbar.appendChild(elem_title)
|
||||||
|
|
||||||
|
const elem_table = document.createElement('table')
|
||||||
|
const elem_tbody = document.createElement('tbody')
|
||||||
|
elems_roledefs.forEach((elem)=>elem_tbody.appendChild(elem))
|
||||||
|
|
||||||
|
elem_table.appendChild(elem_tbody)
|
||||||
|
elem_view.appendChild(elem_table)
|
||||||
|
|
||||||
|
elem_root.appendChild(elem_topbar)
|
||||||
|
elem_root.appendChild(elem_view)
|
||||||
|
|
||||||
|
document.body.appendChild(elem_root)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const initiate_cue_handle_result = function (role_key, payload, func_onresult) {
|
||||||
|
const uid = crypto.randomUUID()
|
||||||
|
result_handlers.set(uid, func_onresult)
|
||||||
|
FRAMEROCK_UTILS.transport_send_bytes(new TextEncoder().encode(JSON.stringify([ 'HANDLE_CUE_INIT', { uid, role_key, payload } ])))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const on_open = function () {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const on_message = function (event) {
|
||||||
|
const [ evt_type, evt_data ] = JSON.parse(new TextDecoder().decode(event.data))
|
||||||
|
if (evt_type === 'HANDLE_CUE_RESULT') {
|
||||||
|
const [ uid, result_obj ] = evt_data
|
||||||
|
const func_onresult = result_handlers.get(uid)
|
||||||
|
if (func_onresult === undefined) {
|
||||||
|
console.error(['Unexpected: func_onresult is undefined', { uid }])
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
result_handlers.delete(uid)
|
||||||
|
func_onresult(result_obj)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setup_dom()
|
||||||
|
FRAMEROCK_UTILS.setup_transport({ on_open, on_message })
|
||||||
|
`.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handle_transport_bytes = function (message, utils) {
|
||||||
|
const [ evt_type, evt_data ] = JSON.parse(new TextDecoder().decode(message))
|
||||||
|
if (evt_type === 'HANDLE_CUE_INIT') {
|
||||||
|
const { uid, role_key, payload } = evt_data
|
||||||
|
const send_result = (result_obj) => {
|
||||||
|
utils.transport_send_bytes(new TextEncoder().encode(JSON.stringify([ 'HANDLE_CUE_RESULT', [ uid, result_obj ] ])))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const on_cue = role_defs[role_key]
|
||||||
|
if (on_cue === undefined) {
|
||||||
|
console.error(['Unexpected: on_cue is undefined', { role_key }])
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (isAsyncFunction(on_cue)) {
|
||||||
|
on_cue(payload).then((result_complete) => {
|
||||||
|
send_result({ result_complete })
|
||||||
|
}).catch((err) => {
|
||||||
|
console.error(err)
|
||||||
|
send_result({ result_error: true })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
let result_complete
|
||||||
|
let did_error = false
|
||||||
|
try {
|
||||||
|
result_complete = on_cue(payload)
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
did_error = true
|
||||||
|
console.error(err)
|
||||||
|
}
|
||||||
|
if (did_error === false) {
|
||||||
|
send_result({ result_complete })
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
send_result({ result_error: true })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await async_run({ config: framerock_config, jsbuild_app_frontend, handle_transport_bytes })
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export { async_stage }
|
Loading…
Add table
Reference in a new issue